diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..79c113f --- /dev/null +++ b/.gitignore @@ -0,0 +1,45 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.build/ +.buildlog/ +.history +.svn/ +.swiftpm/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.pub-cache/ +.pub/ +/build/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/.metadata b/.metadata new file mode 100644 index 0000000..878648b --- /dev/null +++ b/.metadata @@ -0,0 +1,30 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "ea121f8859e4b13e47a8f845e4586164519588bc" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: ea121f8859e4b13e47a8f845e4586164519588bc + base_revision: ea121f8859e4b13e47a8f845e4586164519588bc + - platform: android + create_revision: ea121f8859e4b13e47a8f845e4586164519588bc + base_revision: ea121f8859e4b13e47a8f845e4586164519588bc + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index a756767..b2c6405 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -8,8 +8,7 @@ plugins { android { namespace = "de.holzleitner.liefer.hl_lieferservice" compileSdk = flutter.compileSdkVersion - //ndkVersion = flutter.ndkVersion - ndkVersion = "27.0.12077973" + ndkVersion = flutter.ndkVersion compileOptions { sourceCompatibility = JavaVersion.VERSION_11 diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index a522d24..ca93959 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,4 +1,19 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/android/settings.gradle.kts b/android/settings.gradle.kts index a439442..11662c3 100644 --- a/android/settings.gradle.kts +++ b/android/settings.gradle.kts @@ -19,7 +19,7 @@ pluginManagement { plugins { id("dev.flutter.flutter-plugin-loader") version "1.0.0" id("com.android.application") version "8.7.0" apply false - id("org.jetbrains.kotlin.android") version "1.8.22" apply false + id("org.jetbrains.kotlin.android") version "2.1.0" apply false } include(":app") diff --git a/assets/hl_server_config.json b/assets/hl_server_config.json index 6403941..d34e349 100644 --- a/assets/hl_server_config.json +++ b/assets/hl_server_config.json @@ -1,7 +1,3 @@ { - "host": "http://192.168.1.9:8334", - "user": "GSDWebServiceTmp", - "pass": "098f6bcd4621d373cade4e832627b4f6", - "appKey": "GSD-RestApi", - "appNames": ["GSD-RestApi"] + "backendUrl": "http://192.168.1.9:3000" } \ No newline at end of file diff --git a/ios/Flutter/AppFrameworkInfo.plist b/ios/Flutter/AppFrameworkInfo.plist index 7c56964..1dc6cf7 100644 --- a/ios/Flutter/AppFrameworkInfo.plist +++ b/ios/Flutter/AppFrameworkInfo.plist @@ -21,6 +21,6 @@ CFBundleVersion 1.0 MinimumOSVersion - 12.0 + 13.0 diff --git a/ios/Podfile b/ios/Podfile index e549ee2..620e46e 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -# platform :ios, '12.0' +# platform :ios, '13.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 1dc7670..35cd0bb 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -346,7 +346,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -473,7 +473,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -524,7 +524,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 15cada4..e3773d4 100644 --- a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -26,6 +26,7 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit" shouldUseLaunchSchemeArgsEnv = "YES"> + + NSCameraUsageDescription + Wir benötigen Zugriff auf deine Kamera zum Scannen von Barcodes. + + + NSLocationWhenInUseUsageDescription + Diese App benötigt keinen Standortzugriff. + + NSLocationWhenInUseUsageDescription + Diese App benötigt deinen Standort, um die Lieferdistanz zu berechnen. + + NSLocationAlwaysAndWhenInUseUsageDescription + Diese App benötigt deinen Standort, um die Lieferdistanz zu berechnen. CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName @@ -45,5 +58,17 @@ UIApplicationSupportsIndirectInputEvents + + CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLSchemes + + myapp + + + diff --git a/lib/bloc/app_bloc.dart b/lib/bloc/app_bloc.dart index 0a45173..8f199a4 100644 --- a/lib/bloc/app_bloc.dart +++ b/lib/bloc/app_bloc.dart @@ -4,6 +4,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:hl_lieferservice/bloc/app_events.dart'; import 'package:hl_lieferservice/bloc/app_states.dart'; +import 'package:hl_lieferservice/main.dart'; import 'package:hl_lieferservice/repository/config.dart'; import '../services/erpframe.dart'; @@ -23,8 +24,11 @@ class AppBloc extends Bloc { repository.setDocuFrameConfiguration(configuration); + var config = await repository.getDocuFrameConfiguration(); + locator.registerSingleton(config); + emit( - AppConfigLoaded(config: await repository.getDocuFrameConfiguration()), + AppConfigLoaded(config: config), ); } catch (e) { emit( diff --git a/lib/dto/delivery_update.dart b/lib/dto/delivery_update.dart index 7596034..2f0bd3d 100644 --- a/lib/dto/delivery_update.dart +++ b/lib/dto/delivery_update.dart @@ -1,27 +1,52 @@ import 'package:hl_lieferservice/model/delivery.dart'; +import 'package:intl/intl.dart'; import 'package:json_annotation/json_annotation.dart'; part 'delivery_update.g.dart'; +@JsonSerializable(fieldRename: FieldRename.snake) +class DeliveryOptionUpdateDTO { + DeliveryOptionUpdateDTO({ + required this.numerical, + required this.value, + required this.key, + }); + + bool numerical; + String value; + String key; + + factory DeliveryOptionUpdateDTO.fromJson(Map json) => + _$DeliveryOptionUpdateDTOFromJson(json); + + Map toJson() => _$DeliveryOptionUpdateDTOToJson(this); + + factory DeliveryOptionUpdateDTO.fromEntity(DeliveryOption option) { + return DeliveryOptionUpdateDTO( + numerical: option.numerical, + value: option.value, + key: option.key, + ); + } +} + @JsonSerializable(fieldRename: FieldRename.snake) class DeliveryUpdateDTO { DeliveryUpdateDTO({ required this.deliveryId, - this.note, this.finishedDate, - this.discount, this.selectedPaymentMethodId, + this.options, this.state, this.carId, }); String deliveryId; - String? note; String? finishedDate; String? state; - int? carId; + String? carId; String? selectedPaymentMethodId; - double? discount; + List? options; factory DeliveryUpdateDTO.fromJson(Map json) => _$DeliveryUpdateDTOFromJson(json); @@ -47,7 +72,10 @@ class DeliveryUpdateDTO { return DeliveryUpdateDTO( deliveryId: delivery.id, state: state, - carId: delivery.carId, + carId: delivery.carId?.toString() , + selectedPaymentMethodId: delivery.payment.id, + options: delivery.options.map(DeliveryOptionUpdateDTO.fromEntity).toList(), + finishedDate: DateTime.now().millisecondsSinceEpoch.toString() ); } diff --git a/lib/dto/delivery_update.g.dart b/lib/dto/delivery_update.g.dart index 9451ee0..aa2e18a 100644 --- a/lib/dto/delivery_update.g.dart +++ b/lib/dto/delivery_update.g.dart @@ -6,24 +6,44 @@ part of 'delivery_update.dart'; // JsonSerializableGenerator // ************************************************************************** +DeliveryOptionUpdateDTO _$DeliveryOptionUpdateDTOFromJson( + Map json, +) => DeliveryOptionUpdateDTO( + numerical: json['numerical'] as bool, + value: json['value'] as String, + key: json['key'] as String, +); + +Map _$DeliveryOptionUpdateDTOToJson( + DeliveryOptionUpdateDTO instance, +) => { + 'numerical': instance.numerical, + 'value': instance.value, + 'key': instance.key, +}; + DeliveryUpdateDTO _$DeliveryUpdateDTOFromJson(Map json) => DeliveryUpdateDTO( deliveryId: json['delivery_id'] as String, - note: json['note'] as String?, finishedDate: json['finished_date'] as String?, - discount: (json['discount'] as num?)?.toDouble(), selectedPaymentMethodId: json['selected_payment_method_id'] as String?, + options: + (json['options'] as List?) + ?.map( + (e) => + DeliveryOptionUpdateDTO.fromJson(e as Map), + ) + .toList(), state: json['state'] as String?, - carId: (json['car_id'] as num?)?.toInt(), + carId: json['car_id'] as String?, ); Map _$DeliveryUpdateDTOToJson(DeliveryUpdateDTO instance) => { 'delivery_id': instance.deliveryId, - 'note': instance.note, 'finished_date': instance.finishedDate, 'state': instance.state, 'car_id': instance.carId, 'selected_payment_method_id': instance.selectedPaymentMethodId, - 'discount': instance.discount, + 'options': instance.options, }; diff --git a/lib/exceptions.dart b/lib/exceptions.dart new file mode 100644 index 0000000..9140a6b --- /dev/null +++ b/lib/exceptions.dart @@ -0,0 +1 @@ +class AppConfigNotFound implements Exception {} \ No newline at end of file diff --git a/lib/feature/authentication/bloc/auth_bloc.dart b/lib/feature/authentication/bloc/auth_bloc.dart index eeeea86..d2157ac 100644 --- a/lib/feature/authentication/bloc/auth_bloc.dart +++ b/lib/feature/authentication/bloc/auth_bloc.dart @@ -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 { - UserRepository repository; + UserInfoService service; OperationBloc operationBloc; - AuthBloc({required this.repository, required this.operationBloc}) - : super(Unauthenticated()) { - on(_auth); + AuthBloc({required this.service, required this.operationBloc}) + : super(Unauthenticated()) { + on(_auth); on(_logout); } - Future _auth(Authenticate event, Emitter emit) async { + Future _auth( + SetAuthenticatedEvent event, + Emitter 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(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 _logout(Logout event, Emitter emit) async { diff --git a/lib/feature/authentication/bloc/auth_event.dart b/lib/feature/authentication/bloc/auth_event.dart index 048ef8c..bbbb6e3 100644 --- a/lib/feature/authentication/bloc/auth_event.dart +++ b/lib/feature/authentication/bloc/auth_event.dart @@ -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 { diff --git a/lib/feature/authentication/bloc/auth_state.dart b/lib/feature/authentication/bloc/auth_state.dart index 1ecba2b..3b2b1c8 100644 --- a/lib/feature/authentication/bloc/auth_state.dart +++ b/lib/feature/authentication/bloc/auth_state.dart @@ -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}); +} \ No newline at end of file diff --git a/lib/feature/authentication/exceptions.dart b/lib/feature/authentication/exceptions.dart new file mode 100644 index 0000000..fea231a --- /dev/null +++ b/lib/feature/authentication/exceptions.dart @@ -0,0 +1 @@ +class UserUnauthorized implements Exception {} \ No newline at end of file diff --git a/lib/feature/authentication/model/user.dart b/lib/feature/authentication/model/user.dart new file mode 100644 index 0000000..3d4e73a --- /dev/null +++ b/lib/feature/authentication/model/user.dart @@ -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 json) => + _$UserFromJson(json); + + Map toJson() => _$UserToJson(this); +} \ No newline at end of file diff --git a/lib/feature/authentication/model/user.g.dart b/lib/feature/authentication/model/user.g.dart new file mode 100644 index 0000000..b036379 --- /dev/null +++ b/lib/feature/authentication/model/user.g.dart @@ -0,0 +1,21 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'user.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +User _$UserFromJson(Map json) => User( + number: json['number'] as String, + firstName: json['firstName'] as String, + lastName: json['lastName'] as String, + mail: json['mail'] as String, +); + +Map _$UserToJson(User instance) => { + 'number': instance.number, + 'lastName': instance.lastName, + 'firstName': instance.firstName, + 'mail': instance.mail, +}; diff --git a/lib/feature/authentication/presentation/login_page.dart b/lib/feature/authentication/presentation/login_page.dart index ce6ff5c..5901320 100644 --- a/lib/feature/authentication/presentation/login_page.dart +++ b/lib/feature/authentication/presentation/login_page.dart @@ -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 { final _loginFormKey = GlobalKey(); - final TextEditingController _passwordEditingController = - TextEditingController(); - final TextEditingController _userIdEditingController = - TextEditingController(); + bool _isLoading = false; + late AppLinks _appLinks; + StreamSubscription? _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().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(); + + // 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().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 { 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 { ), ); } -} +} \ No newline at end of file diff --git a/lib/feature/authentication/service/userinfo.dart b/lib/feature/authentication/service/userinfo.dart new file mode 100644 index 0000000..a0f114b --- /dev/null +++ b/lib/feature/authentication/service/userinfo.dart @@ -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 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)); + } +} \ No newline at end of file diff --git a/lib/feature/cars/bloc/cars_state.dart b/lib/feature/cars/bloc/cars_state.dart index 6a09593..3314f2f 100644 --- a/lib/feature/cars/bloc/cars_state.dart +++ b/lib/feature/cars/bloc/cars_state.dart @@ -8,6 +8,24 @@ class CarsLoading extends CarsState {} class CarsLoadingFailed extends CarsState {} +class CarAdded extends CarsState { + Car car; + + CarAdded({required this.car}); +} + +class CarDeleted extends CarsState { + String plate; + + CarDeleted({required this.plate}); +} + +class CarEdited extends CarsState { + Car car; + + CarEdited({required this.car}); +} + class CarsLoaded extends CarsState { List cars; String teamId; @@ -15,9 +33,6 @@ class CarsLoaded extends CarsState { CarsLoaded({required this.cars, required this.teamId}); CarsLoaded copyWith({List? cars, String? teamId}) { - return CarsLoaded( - cars: cars ?? this.cars, - teamId: teamId ?? this.teamId, - ); + return CarsLoaded(cars: cars ?? this.cars, teamId: teamId ?? this.teamId); } -} \ No newline at end of file +} diff --git a/lib/feature/cars/presentation/car_management_page.dart b/lib/feature/cars/presentation/car_management_page.dart index 08fb93f..a1d354e 100644 --- a/lib/feature/cars/presentation/car_management_page.dart +++ b/lib/feature/cars/presentation/car_management_page.dart @@ -6,6 +6,9 @@ import 'package:hl_lieferservice/feature/cars/bloc/cars_bloc.dart'; import 'package:hl_lieferservice/feature/cars/bloc/cars_event.dart'; import 'package:hl_lieferservice/feature/cars/bloc/cars_state.dart'; import 'package:hl_lieferservice/feature/cars/presentation/car_management.dart'; +import 'package:hl_lieferservice/feature/delivery/overview/bloc/tour_bloc.dart'; +import 'package:hl_lieferservice/feature/delivery/overview/bloc/tour_event.dart'; +import 'package:hl_lieferservice/feature/delivery/overview/bloc/tour_state.dart'; import 'package:hl_lieferservice/model/car.dart'; class CarManagementPage extends StatefulWidget { @@ -24,18 +27,18 @@ class _CarManagementPageState extends State { // Load cars _authState = context.read().state as Authenticated; - context.read().add(CarLoad(teamId: _authState.teamId)); + context.read().add(CarLoad(teamId: _authState.user.number)); } void _add(String plate) { context.read().add( - CarAdd(teamId: _authState.teamId, plate: plate), + CarAdd(teamId: _authState.user.number, plate: plate), ); } void _remove(String id) { context.read().add( - CarDelete(carId: id, teamId: _authState.teamId), + CarDelete(carId: id, teamId: _authState.user.number), ); } @@ -43,7 +46,7 @@ class _CarManagementPageState extends State { context.read().add( CarEdit( newCar: Car(id: int.parse(id), plate: plate), - teamId: _authState.teamId, + teamId: _authState.user.number, ), ); } @@ -51,7 +54,14 @@ class _CarManagementPageState extends State { @override Widget build(BuildContext context) { return Scaffold( - body: BlocBuilder( + body: BlocConsumer( + listener: (context, state) { + if (state is CarsLoaded) { + var tour = (context.read().state as TourLoaded).tour.copyWith(); + tour.driver.cars = state.cars; + context.read().add(UpdateTour(tour: tour)); + } + }, builder: (context, state) { debugPrint('BlocBuilder rebuilding with state: $state'); diff --git a/lib/feature/cars/service/cars_service.dart b/lib/feature/cars/service/cars_service.dart index b086661..337acc6 100644 --- a/lib/feature/cars/service/cars_service.dart +++ b/lib/feature/cars/service/cars_service.dart @@ -1,8 +1,11 @@ import 'dart:convert'; +import 'dart:io'; -import 'package:docuframe/docuframe.dart' as df; import 'package:flutter/cupertino.dart'; +import 'package:hl_lieferservice/feature/authentication/exceptions.dart'; import 'package:hl_lieferservice/services/erpframe.dart'; +import 'package:hl_lieferservice/util.dart'; +import 'package:http/http.dart'; import '../../../dto/basic_response.dart'; import '../../../dto/car_add.dart'; @@ -14,23 +17,31 @@ class CarService extends ErpFrameService { CarService({required super.config}); Future addCar(String plate, int teamId) async { - df.LoginSession? session; - try { - session = await getSession(); - df.DocuFrameMacroResponse response = - await df.Macro(config: dfConfig, session: session).execute( - "_web_addCar", - parameter: CarAddDTO.make(teamId, plate).toJson() - as Map); + debugPrint(jsonEncode({"team_id": teamId.toString(), "plate": plate})); - Map responseJson = jsonDecode(response.body!); - debugPrint(responseJson.toString()); + var response = await post( + urlBuilder("_web_addCar"), + headers: getSessionOrThrow(), + body: {"team_id": teamId.toString(), "plate": plate}, + ); + + if (response.statusCode == HttpStatus.unauthorized) { + throw UserUnauthorized(); + } + + var body = response.body; + + debugPrint("BODY: $body"); + + Map responseJson = jsonDecode(body); CarAddResponseDTO responseDto = CarAddResponseDTO.fromJson(responseJson); if (responseDto.succeeded == true) { return Car( - id: int.parse(responseDto.car.id), plate: responseDto.car.plate); + id: int.parse(responseDto.car.id), + plate: responseDto.car.plate, + ); } else { throw responseDto.message; } @@ -40,26 +51,22 @@ class CarService extends ErpFrameService { debugPrint(st.toString()); rethrow; - } finally { - await logout(session); } } Future editCar(Car car) async { - df.LoginSession? session; - try { - session = await getSession(); + var response = await post( + urlBuilder("_web_editCar"), + headers: getSessionOrThrow(), + body: {"id": car.id.toString(), "plate": car.plate}, + ); - debugPrint(car.plate); - debugPrint(car.id.toString()); + if (response.statusCode == HttpStatus.unauthorized) { + throw UserUnauthorized(); + } - df.DocuFrameMacroResponse response = - await df.Macro(config: dfConfig, session: session).execute( - "_web_editCar", - parameter: {"id": car.id, "plate": car.plate}); - - Map responseJson = jsonDecode(response.body!); + Map responseJson = jsonDecode(response.body); debugPrint(responseJson.toString()); BasicResponseDTO responseDto = BasicResponseDTO.fromJson(responseJson); @@ -75,22 +82,22 @@ class CarService extends ErpFrameService { debugPrint(st.toString()); rethrow; - } finally { - await logout(session); } } Future removeCar(int carId, int teamId) async { - df.LoginSession? session; - try { - session = await getSession(); - df.DocuFrameMacroResponse response = - await df.Macro(config: dfConfig, session: session).execute( - "_web_removeCar", - parameter: {"team_id": teamId, "id": carId}); + var response = await post( + urlBuilder("_web_removeCar"), + headers: getSessionOrThrow(), + body: {"team_id": teamId.toString(), "id": carId.toString()}, + ); - Map responseJson = jsonDecode(response.body!); + if (response.statusCode == HttpStatus.unauthorized) { + throw UserUnauthorized(); + } + + Map responseJson = jsonDecode(response.body); debugPrint(responseJson.toString()); BasicResponseDTO responseDto = BasicResponseDTO.fromJson(responseJson); @@ -105,25 +112,28 @@ class CarService extends ErpFrameService { debugPrint(st.toString()); rethrow; - } finally { - await logout(session); } } Future> getCars(int teamId) async { - df.LoginSession? session; - try { - session = await getSession(); - df.DocuFrameMacroResponse response = - await df.Macro(config: dfConfig, session: session) - .execute("_web_getCars", parameter: {"team_id": teamId}); - debugPrint(teamId.toString()); - Map responseJson = jsonDecode(response.body!); - debugPrint("RESPONSE"); - debugPrint(responseJson.toString()); + var response = await post( + urlBuilder("_web_getCars"), + headers: getSessionOrThrow(), + body:{"team_id": teamId.toString()}, + ); + + if (response.statusCode == HttpStatus.unauthorized) { + throw UserUnauthorized(); + } + + var body = response.body; + + debugPrint("BODY: $body"); + + Map responseJson = jsonDecode(response.body); CarGetResponseDTO responseDto = CarGetResponseDTO.fromJson(responseJson); if (responseDto.succeeded == true) { @@ -139,8 +149,6 @@ class CarService extends ErpFrameService { debugPrint(st.toString()); rethrow; - } finally { - await logout(session); } } } diff --git a/lib/feature/delivery/detail/bloc/delivery_bloc.dart b/lib/feature/delivery/detail/bloc/delivery_bloc.dart index 3e4754a..6eae017 100644 --- a/lib/feature/delivery/detail/bloc/delivery_bloc.dart +++ b/lib/feature/delivery/detail/bloc/delivery_bloc.dart @@ -5,6 +5,7 @@ import 'package:hl_lieferservice/dto/discount_update_response.dart'; import 'package:hl_lieferservice/feature/delivery/detail/bloc/delivery_event.dart'; import 'package:hl_lieferservice/feature/delivery/detail/bloc/delivery_state.dart'; import 'package:hl_lieferservice/feature/delivery/detail/repository/delivery_repository.dart'; +import 'package:hl_lieferservice/feature/delivery/detail/repository/note_repository.dart'; import 'package:hl_lieferservice/widget/operations/bloc/operation_bloc.dart'; import 'package:hl_lieferservice/widget/operations/bloc/operation_event.dart'; @@ -15,21 +16,62 @@ import '../../../../model/delivery.dart' as model; class DeliveryBloc extends Bloc { OperationBloc opBloc; DeliveryRepository repository; + NoteRepository noteRepository; - DeliveryBloc({required this.opBloc, required this.repository}) - : super(DeliveryInitial()) { + DeliveryBloc({ + required this.opBloc, + required this.repository, + required this.noteRepository, + }) : super(DeliveryInitial()) { on(_unscan); on(_resetAmount); on(_load); on(_addDiscount); on(_removeDiscount); on(_updateDiscount); - on(_updateDeliveryOptions); - on(_updatePayment); + on(_updateDeliveryOptions); + on(_updatePayment); + on(_finishDelivery); + } + + void _finishDelivery( + FinishDeliveryEvent event, + Emitter emit, + ) async { + final currentState = state; + opBloc.add(LoadOperation()); + + if (currentState is DeliveryLoaded) { + try { + model.Delivery newDelivery = event.delivery.copyWith(); + newDelivery.state = model.DeliveryState.finished; + + for (final option in event.delivery.options) { + debugPrint("VALUE=${option.value};KEY=${option.key}"); + } + + await repository.updateDelivery(newDelivery); + await noteRepository.addNamedImage( + event.delivery.id, + event.driverSignature, + "delivery_${event.delivery.id}_signature_driver.jpg", + ); + await noteRepository.addNamedImage( + event.delivery.id, + event.customerSignature, + "delivery_${event.delivery.id}_signature_customer.jpg", + ); + emit(DeliveryFinished(delivery: newDelivery)); + opBloc.add(FinishOperation()); + } catch (e, st) { + opBloc.add(FailOperation(message: "Failed to update delivery")); + debugPrint(st.toString()); + } + } } void _updatePayment( - UpdateSelectedPaymentMethod event, + UpdateSelectedPaymentMethodEvent event, Emitter emit, ) { final currentState = state; @@ -44,7 +86,7 @@ class DeliveryBloc extends Bloc { } void _updateDeliveryOptions( - UpdateDeliveryOption event, + UpdateDeliveryOptionEvent event, Emitter emit, ) { final currentState = state; @@ -53,7 +95,11 @@ class DeliveryBloc extends Bloc { List options = currentState.delivery.options.map((option) { if (option.key == event.key) { - return option.copyWith(value: event.value.toString()); + if (option.numerical) { + return option.copyWith(value: event.value); + } else { + return option.copyWith(value: event.value == true ? "1" : "0"); + } } return option; diff --git a/lib/feature/delivery/detail/bloc/delivery_event.dart b/lib/feature/delivery/detail/bloc/delivery_event.dart index 14442db..8b43caf 100644 --- a/lib/feature/delivery/detail/bloc/delivery_event.dart +++ b/lib/feature/delivery/detail/bloc/delivery_event.dart @@ -1,3 +1,5 @@ +import 'dart:typed_data'; + import 'package:hl_lieferservice/model/delivery.dart'; import 'package:hl_lieferservice/model/tour.dart'; @@ -57,15 +59,27 @@ class UpdateDiscountEvent extends DeliveryEvent { int? value; } -class UpdateDeliveryOption extends DeliveryEvent { - UpdateDeliveryOption({required this.key, required this.value}); +class UpdateDeliveryOptionEvent extends DeliveryEvent { + UpdateDeliveryOptionEvent({required this.key, required this.value}); String key; dynamic value; } -class UpdateSelectedPaymentMethod extends DeliveryEvent { - UpdateSelectedPaymentMethod({required this.payment}); +class UpdateSelectedPaymentMethodEvent extends DeliveryEvent { + UpdateSelectedPaymentMethodEvent({required this.payment}); Payment payment; } + +class FinishDeliveryEvent extends DeliveryEvent { + FinishDeliveryEvent({ + required this.delivery, + required this.driverSignature, + required this.customerSignature, + }); + + Delivery delivery; + Uint8List customerSignature; + Uint8List driverSignature; +} \ No newline at end of file diff --git a/lib/feature/delivery/detail/bloc/delivery_state.dart b/lib/feature/delivery/detail/bloc/delivery_state.dart index 0ebf874..4d1f8f3 100644 --- a/lib/feature/delivery/detail/bloc/delivery_state.dart +++ b/lib/feature/delivery/detail/bloc/delivery_state.dart @@ -12,4 +12,14 @@ class DeliveryLoaded extends DeliveryState { DeliveryLoaded copyWith(Delivery? delivery) { return DeliveryLoaded(delivery: delivery ?? this.delivery); } -} \ No newline at end of file +} + +class DeliveryFinished extends DeliveryState { + DeliveryFinished({required this.delivery}); + + Delivery delivery; + + DeliveryFinished copyWith(Delivery? delivery) { + return DeliveryFinished(delivery: delivery ?? this.delivery); + } +} diff --git a/lib/feature/delivery/detail/bloc/note_event.dart b/lib/feature/delivery/detail/bloc/note_event.dart index 2064123..044b956 100644 --- a/lib/feature/delivery/detail/bloc/note_event.dart +++ b/lib/feature/delivery/detail/bloc/note_event.dart @@ -41,4 +41,4 @@ class RemoveImageNote extends NoteEvent { final String objectId; final String deliveryId; -} +} \ No newline at end of file diff --git a/lib/feature/delivery/detail/exceptions.dart b/lib/feature/delivery/detail/exceptions.dart new file mode 100644 index 0000000..026c8b3 --- /dev/null +++ b/lib/feature/delivery/detail/exceptions.dart @@ -0,0 +1 @@ +class NoteImageAddException implements Exception {} \ No newline at end of file diff --git a/lib/feature/delivery/detail/presentation/delivery_detail_page.dart b/lib/feature/delivery/detail/presentation/delivery_detail_page.dart index 50a4417..bdd9d41 100644 --- a/lib/feature/delivery/detail/presentation/delivery_detail_page.dart +++ b/lib/feature/delivery/detail/presentation/delivery_detail_page.dart @@ -8,6 +8,9 @@ import 'package:hl_lieferservice/feature/delivery/detail/bloc/delivery_event.dar import 'package:hl_lieferservice/feature/delivery/detail/bloc/delivery_state.dart'; import 'package:hl_lieferservice/feature/delivery/detail/presentation/delivery_sign.dart'; import 'package:hl_lieferservice/feature/delivery/detail/presentation/steps/step.dart'; +import 'package:hl_lieferservice/feature/delivery/overview/bloc/tour_bloc.dart'; +import 'package:hl_lieferservice/feature/delivery/overview/bloc/tour_event.dart'; +import 'package:hl_lieferservice/feature/delivery/overview/bloc/tour_state.dart'; import 'package:hl_lieferservice/model/delivery.dart' as model; class DeliveryDetail extends StatefulWidget { @@ -126,7 +129,14 @@ class _DeliveryDetailState extends State { } void _onSign(Uint8List customer, Uint8List driver) async { - + final currentState = context.read().state as DeliveryLoaded; + context.read().add( + FinishDeliveryEvent( + delivery: currentState.delivery, + customerSignature: customer, + driverSignature: driver, + ), + ); } Widget _stepsNavigation() { @@ -143,7 +153,10 @@ class _DeliveryDetailState extends State { Padding( padding: const EdgeInsets.only(left: 20), child: FilledButton( - onPressed: _step == _steps.length - 1 ? _openSignatureView : _clickForward, + onPressed: + _step == _steps.length - 1 + ? _openSignatureView + : _clickForward, child: _step == _steps.length - 1 ? const Text("Unterschreiben") @@ -159,7 +172,24 @@ class _DeliveryDetailState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text("Auslieferungsdetails")), - body: BlocBuilder( + body: BlocConsumer( + listener: (context, state) { + if (state is DeliveryFinished) { + final tourState = context.read().state as TourLoaded; + final newTour = tourState.tour.copyWith(deliveries: tourState.tour.deliveries.map((delivery) { + if (delivery.id == state.delivery.id) { + return state.delivery; + } + + return delivery; + }).toList()); + + context.read().add(UpdateTour(tour: newTour)); + + Navigator.pop(context); + Navigator.pop(context); + } + }, builder: (context, state) { final currentState = state; diff --git a/lib/feature/delivery/detail/presentation/delivery_options.dart b/lib/feature/delivery/detail/presentation/delivery_options.dart index 85add48..2e0cb0b 100644 --- a/lib/feature/delivery/detail/presentation/delivery_options.dart +++ b/lib/feature/delivery/detail/presentation/delivery_options.dart @@ -25,17 +25,34 @@ class _DeliveryOptionsViewState extends State { } void _update(model.DeliveryOption option, dynamic value) { + debugPrint(option.key); + + if (value is bool) { + context.read().add( + UpdateDeliveryOptionEvent(key: option.key, value: !value), + ); + + return; + } + context.read().add( - UpdateDeliveryOption(key: option.key, value: value), + UpdateDeliveryOptionEvent(key: option.key, value: value), ); + } List _options() { List boolOptions = widget.options.where((option) => !option.numerical).map((option) { + debugPrint("Value: ${option.value}, Key: ${option.key}"); + return CheckboxListTile( - value: option.getValue() as bool, - onChanged: (value) => _update(option, value), + value: option.getValue(), + onChanged: (value) { + debugPrint("HAHAHA"); + debugPrint(value.toString()); + _update(option, option.getValue()); + }, title: Text(option.display), ); }).toList(); @@ -49,7 +66,9 @@ class _DeliveryOptionsViewState extends State { initialValue: option.getValue().toString(), keyboardType: TextInputType.number, onTapOutside: (event) => FocusScope.of(context).unfocus(), - onChanged: (value) => _update(option, value), + onChanged: (value) { + _update(option, value); + }, ), ); }).toList(); diff --git a/lib/feature/delivery/detail/presentation/delivery_summary.dart b/lib/feature/delivery/detail/presentation/delivery_summary.dart index 3dc8e93..e66b8d1 100644 --- a/lib/feature/delivery/detail/presentation/delivery_summary.dart +++ b/lib/feature/delivery/detail/presentation/delivery_summary.dart @@ -98,7 +98,7 @@ class _DeliverySummaryState extends State { initialSelection: widget.delivery.payment.id, onSelected: (id) { context.read().add( - UpdateSelectedPaymentMethod( + UpdateSelectedPaymentMethodEvent( payment: _paymentMethods.firstWhere( (payment) => payment.id == id, ), @@ -108,10 +108,6 @@ class _DeliverySummaryState extends State { ); } - Widget _payment() { - return _paymentOptions(); - } - Widget _paymentDone() { return DecoratedBox( decoration: BoxDecoration( @@ -174,7 +170,7 @@ class _DeliverySummaryState extends State { ), ), - Padding(padding: insets, child: _payment()), + Padding(padding: insets, child: _paymentOptions()), ], ), ); diff --git a/lib/feature/delivery/detail/presentation/note/note_overview.dart b/lib/feature/delivery/detail/presentation/note/note_overview.dart index c553e0b..26b1589 100644 --- a/lib/feature/delivery/detail/presentation/note/note_overview.dart +++ b/lib/feature/delivery/detail/presentation/note/note_overview.dart @@ -39,6 +39,8 @@ class _NoteOverviewState extends State { } Widget _images() { + debugPrint("IMAGES: ${widget.images}"); + return NoteImageOverview( images: widget.images, deliveryId: widget.deliveryId, diff --git a/lib/feature/delivery/detail/presentation/steps/step_info.dart b/lib/feature/delivery/detail/presentation/steps/step_info.dart index 3e74b75..4d8803a 100644 --- a/lib/feature/delivery/detail/presentation/steps/step_info.dart +++ b/lib/feature/delivery/detail/presentation/steps/step_info.dart @@ -1,7 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:hl_lieferservice/feature/delivery/overview/bloc/tour_event.dart'; import 'package:hl_lieferservice/model/article.dart'; import 'package:hl_lieferservice/model/delivery.dart'; +import 'package:url_launcher/url_launcher.dart'; import '../../../overview/bloc/tour_bloc.dart'; import '../../../overview/bloc/tour_state.dart'; @@ -16,6 +18,100 @@ class DeliveryStepInfo extends StatefulWidget { } class _DeliveryStepInfo extends State { + void _launchMapsUrl(String mapsApp) async { + final address = widget.delivery.customer.address.toString(); + final encodedAddress = Uri.encodeComponent(address); + Uri url; + + switch (mapsApp) { + case 'google': + url = Uri.parse( + 'https://www.google.com/maps/search/?api=1&query=$encodedAddress', + ); + break; + case 'apple': + url = Uri.parse('http://maps.apple.com/?daddr=$encodedAddress'); + break; + default: + return; + } + + await launchUrl(url, mode: LaunchMode.externalApplication); + } + + Widget _deliveryStatusChangeActions() { + List actions = []; + + if (widget.delivery.state == DeliveryState.ongoing) { + actions = [ + Column( + children: [ + IconButton( + onPressed: () { + context.read().add( + HoldDeliveryEvent(deliveryId: widget.delivery.id), + ); + Navigator.of(context).pop(); + }, + icon: Icon( + Icons.change_circle, + color: Colors.orangeAccent, + size: 42, + ), + ), + Text("Zurückstellen"), + ], + ), + + Column( + children: [ + IconButton( + onPressed: () { + context.read().add( + CancelDeliveryEvent(deliveryId: widget.delivery.id), + ); + + Navigator.of(context).pop(); + }, + //style: IconButton.styleFrom(backgroundColor: Colors.red), + icon: Icon(Icons.cancel, color: Colors.red, size: 42), + ), + Text("Abbrechen"), + ], + ), + ]; + } + + if (widget.delivery.state == DeliveryState.canceled || + widget.delivery.state == DeliveryState.onhold || + widget.delivery.state == DeliveryState.finished) { + actions = [ + Column( + children: [ + IconButton( + onPressed: () { + context.read().add( + ReactivateDeliveryEvent(deliveryId: widget.delivery.id), + ); + }, + icon: Icon( + Icons.published_with_changes, + color: Colors.blueAccent, + size: 42 + ), + ), + Text("Reaktivieren"), + ], + ), + ]; + } + + return Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: actions, + ); + } + Widget _fastActions() { return SizedBox( width: double.infinity, @@ -23,25 +119,55 @@ class _DeliveryStepInfo extends State { color: Theme.of(context).colorScheme.onSecondary, child: Padding( padding: const EdgeInsets.all(10), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, + child: Column( children: [ - Column( + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - IconButton.filled(onPressed: () {}, icon: Icon(Icons.phone)), - Text("Anrufen"), + Column( + children: [ + IconButton.filled( + onPressed: + widget.delivery.contactPerson?.phoneNumber != null + ? () async { + await launchUrl( + Uri( + scheme: "tel", + path: + widget + .delivery + .contactPerson + ?.phoneNumber!, + ), + ); + } + : null, + icon: Icon(Icons.phone), + ), + Text("Anrufen"), + ], + ), + + Column( + children: [ + IconButton.filled( + onPressed: () { + _launchMapsUrl("google"); + }, + icon: Icon(Icons.map_outlined), + ), + Text("Google Maps"), + ], + ), ], ), - Column( - children: [ - IconButton.filled( - onPressed: () {}, - icon: Icon(Icons.map_outlined), - ), - Text("Navigation starten"), - ], + const Padding( + padding: EdgeInsets.only(top: 10, bottom: 10), + child: Divider(), ), + + _deliveryStatusChangeActions(), ], ), ), @@ -149,6 +275,34 @@ class _DeliveryStepInfo extends State { ); } + Widget _deliveryAgreements() { + String agreements = "keine Vereinbarungen getroffen!"; + if (widget.delivery.specialAgreements != null && + widget.delivery.specialAgreements != "") { + agreements = widget.delivery.specialAgreements!; + } + + return Card( + color: Theme.of(context).colorScheme.onSecondary, + child: Padding( + padding: const EdgeInsets.all(10), + child: Row( + children: [ + Padding( + padding: EdgeInsets.all(15), + child: Icon( + Icons.warning, + color: Theme.of(context).primaryColor, + size: 28, + ), + ), + Expanded(child: Text(agreements)), + ], + ), + ), + ); + } + @override Widget build(BuildContext context) { return Container( @@ -167,6 +321,18 @@ class _DeliveryStepInfo extends State { child: _fastActions(), ), + Padding( + padding: const EdgeInsets.only(top: 20), + child: Text( + "Sondervereinbarungen", + style: Theme.of(context).textTheme.headlineSmall, + ), + ), + Padding( + padding: const EdgeInsets.only(top: 10), + child: _deliveryAgreements(), + ), + Padding( padding: const EdgeInsets.only(top: 20), child: Text( diff --git a/lib/feature/delivery/detail/repository/delivery_repository.dart b/lib/feature/delivery/detail/repository/delivery_repository.dart index 1d8feae..9c0c162 100644 --- a/lib/feature/delivery/detail/repository/delivery_repository.dart +++ b/lib/feature/delivery/detail/repository/delivery_repository.dart @@ -2,6 +2,7 @@ import 'package:hl_lieferservice/dto/discount_add_response.dart'; import 'package:hl_lieferservice/dto/discount_remove_response.dart'; import 'package:hl_lieferservice/dto/discount_update_response.dart'; import 'package:hl_lieferservice/feature/delivery/overview/service/delivery_info_service.dart'; +import 'package:hl_lieferservice/model/delivery.dart'; class DeliveryRepository { DeliveryRepository({required this.service}); @@ -35,4 +36,8 @@ class DeliveryRepository { ) { return service.updateDiscount(deliveryId, reason, value); } + + Future updateDelivery(Delivery delivery) { + return service.updateDelivery(delivery); + } } diff --git a/lib/feature/delivery/detail/repository/note_repository.dart b/lib/feature/delivery/detail/repository/note_repository.dart index a250dd0..52b2860 100644 --- a/lib/feature/delivery/detail/repository/note_repository.dart +++ b/lib/feature/delivery/detail/repository/note_repository.dart @@ -52,6 +52,17 @@ class NoteRepository { return ImageNote.make(objectId, fileName); } + Future addNamedImage(String deliveryId, Uint8List bytes, String filename) async { + String objectId = await service.uploadImage( + deliveryId, + filename, + bytes, + "image/png", + ); + + return ImageNote.make(objectId, filename); + } + Future deleteImage(String deliveryId, String objectId) async { await service.removeImage(objectId); } diff --git a/lib/feature/delivery/detail/service/notes_service.dart b/lib/feature/delivery/detail/service/notes_service.dart index a6453d8..a02e7fe 100644 --- a/lib/feature/delivery/detail/service/notes_service.dart +++ b/lib/feature/delivery/detail/service/notes_service.dart @@ -1,32 +1,40 @@ +import 'dart:collection'; import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; import 'package:hl_lieferservice/dto/note_get_response.dart'; +import 'package:hl_lieferservice/feature/delivery/detail/exceptions.dart'; import 'package:hl_lieferservice/services/erpframe.dart'; import 'package:docuframe/docuframe.dart' as df; import 'package:flutter/cupertino.dart'; import 'package:http/http.dart' as http; +import 'package:http_parser/http_parser.dart'; import '../../../../dto/basic_response.dart'; import '../../../../dto/note_add_response.dart'; import '../../../../dto/note_template_response.dart'; import '../../../../model/delivery.dart'; +import '../../../../util.dart'; +import '../../../authentication/exceptions.dart'; class NoteService extends ErpFrameService { NoteService({required super.config}); Future deleteNote(int noteId) async { - df.LoginSession? session; - try { - session = await getSession(); - df.DocuFrameMacroResponse response = await df.Macro( - config: dfConfig, - session: session, - ).execute("_web_deleteNote", parameter: {"id": noteId}); + var response = await http.post( + urlBuilder("_web_deleteNote"), + headers: getSessionOrThrow(), + body: {"id": noteId.toString()}, + ); - Map responseJson = jsonDecode(response.body!); + if (response.statusCode == HttpStatus.unauthorized) { + throw UserUnauthorized(); + } + + Map responseJson = jsonDecode(response.body); + debugPrint("NOTE DELETE: ${response.body}"); BasicResponseDTO responseDto = BasicResponseDTO.fromJson(responseJson); if (responseDto.succeeded == true) { @@ -40,25 +48,22 @@ class NoteService extends ErpFrameService { debugPrint(st.toString()); rethrow; - } finally { - await logout(session); } } Future editNote(Note newNote) async { - df.LoginSession? session; - try { - session = await getSession(); - df.DocuFrameMacroResponse response = await df.Macro( - config: dfConfig, - session: session, - ).execute( - "_web_editNote", - parameter: {"id": newNote.id, "note": newNote.content}, + var response = await http.post( + urlBuilder("_web_editNote"), + headers: getSessionOrThrow(), + body: {"id": newNote.id.toString(), "note": newNote.content}, ); - Map responseJson = jsonDecode(response.body!); + if (response.statusCode == HttpStatus.unauthorized) { + throw UserUnauthorized(); + } + + Map responseJson = jsonDecode(response.body); BasicResponseDTO responseDto = BasicResponseDTO.fromJson(responseJson); if (responseDto.succeeded == true) { @@ -72,22 +77,22 @@ class NoteService extends ErpFrameService { debugPrint(st.toString()); rethrow; - } finally { - await logout(session); } } Future> getNoteTemplates() async { - df.LoginSession? session; - try { - session = await getSession(); - df.DocuFrameMacroResponse response = await df.Macro( - config: dfConfig, - session: session, - ).execute("_web_getNoteTemplates"); + var response = await http.post( + urlBuilder("_web_getNoteTemplates"), + headers: getSessionOrThrow(), + body: {}, + ); - Map responseJson = jsonDecode(response.body!); + if (response.statusCode == HttpStatus.unauthorized) { + throw UserUnauthorized(); + } + + Map responseJson = jsonDecode(response.body); NoteTemplateResponseDTO responseDto = NoteTemplateResponseDTO.fromJson( responseJson, ); @@ -103,23 +108,22 @@ class NoteService extends ErpFrameService { debugPrint(st.toString()); rethrow; - } finally { - await logout(session); } } Future> getNotes(String deliveryId) async { - df.LoginSession? session; - try { - session = await getSession(); - df.DocuFrameMacroResponse response = await df.Macro( - config: dfConfig, - session: session, - ).execute("_web_getNotes", parameter: {"delivery_id": deliveryId}); - debugPrint(deliveryId); + var response = await http.post( + urlBuilder("_web_getNotes"), + headers: getSessionOrThrow(), + body: {"delivery_id": deliveryId}, + ); - Map responseJson = jsonDecode(response.body!); + if (response.statusCode == HttpStatus.unauthorized) { + throw UserUnauthorized(); + } + + Map responseJson = jsonDecode(response.body); debugPrint(responseJson.toString()); NoteGetResponseDTO responseDto = NoteGetResponseDTO.fromJson( responseJson, @@ -138,27 +142,22 @@ class NoteService extends ErpFrameService { debugPrint(st.toString()); rethrow; - } finally { - await logout(session); } } Future addNote(String note, int deliveryId) async { - df.LoginSession? session; - try { - session = await getSession(); - df.DocuFrameMacroResponse response = await df.Macro( - config: dfConfig, - session: session, - ).execute( - "_web_addNote", - parameter: {"receipt_id": deliveryId, "note": note}, + var response = await http.post( + urlBuilder("_web_addNote"), + headers: getSessionOrThrow(), + body: {"receipt_id": deliveryId.toString(), "note": note}, ); - debugPrint(deliveryId.toString()); + if (response.statusCode == HttpStatus.unauthorized) { + throw UserUnauthorized(); + } - Map responseJson = jsonDecode(response.body!); + Map responseJson = jsonDecode(response.body); debugPrint(responseJson.toString()); NoteAddResponseDTO responseDto = NoteAddResponseDTO.fromJson( responseJson, @@ -172,8 +171,6 @@ class NoteService extends ErpFrameService { } } catch (e) { rethrow; - } finally { - await logout(session); } } @@ -183,63 +180,79 @@ class NoteService extends ErpFrameService { Uint8List bytes, String? mimeType, ) async { - df.LoginSession? session; - try { - session = await getSession(); - - // First get UPLOAD ID - df.UploadFile uploadHandler = df.UploadFile( - config: dfConfig, - session: session, + var config = getConfig(); + var basePath = "${config.backendUrl}/v1/uploadFile"; + var response = await http.get( + Uri.parse(basePath), + headers: getSessionOrThrow(), ); - df.GetUploadIdResponse uploadIdResponse = - await uploadHandler.getUploadId(); - // Upload binary data to DOCUframe - debugPrint(filename); - df.FileInformationResponse response = await uploadHandler.uploadFile( - uploadIdResponse.uploadId, - bytes, - filename, - mimeType ?? "image/jpeg", - ); - debugPrint(response.body); + if (response.statusCode == HttpStatus.unauthorized) { + throw UserUnauthorized(); + } - // Commit file upload - df.CommitFileUploadResponse commitResponse = await uploadHandler - .commitUpload(uploadIdResponse.uploadId); - debugPrint(commitResponse.body); + Map jsonResponse = jsonDecode(response.body); + debugPrint("GET UPLOADID : ${response.body}"); - return commitResponse.objectId; + if (!jsonResponse.containsKey("data")) { + debugPrint("No data structure in uploadFile request"); + debugPrint("RAW RESPONSE: ${response.body}"); + throw NoteImageAddException(); + } + + Map data = jsonResponse["data"]; + + if (!data.containsKey("uploadId")) { + debugPrint("No data.uploadId structure in uploadFile request"); + debugPrint("RAW RESPONSE: ${response.body}"); + throw NoteImageAddException(); + } + + String uploadId = data["uploadId"]; + http.MultipartRequest request = + http.MultipartRequest("POST", Uri.parse("$basePath/$uploadId")); + + HashMap header = HashMap(); + header["Content-Type"] = "multipart/form-data"; + header.addAll(getSessionOrThrow()); + + request.headers.addAll(header); + request.files.add(http.MultipartFile.fromBytes("file", bytes, + filename: filename, + contentType: MediaType.parse(mimeType ?? "application/octet-stream"))); + + http.Response fileUploadResponse = await http.Response.fromStream(await request.send()); + Map fileUploadResponseJson = jsonDecode(fileUploadResponse.body); + + debugPrint("UPLOAD IMAGE RESPONSE: ${fileUploadResponse.body}"); + + if (fileUploadResponseJson["status"]["internalStatus"] != "0") { + debugPrint("Failed to upload image"); + debugPrint("RAW: ${fileUploadResponseJson.toString()}"); + throw NoteImageAddException(); + } + + var fileCommitResponse = await http.patch(Uri.parse("$basePath/$uploadId"), headers: getSessionOrThrow()); + debugPrint("FILE COMMIT BODY: ${fileCommitResponse.body}"); + var fileCommitResponseJson = jsonDecode(fileCommitResponse.body); + + return fileCommitResponseJson["data"]["~ObjectID"]; } catch (e, st) { debugPrint("An error occured:"); debugPrint("$e"); debugPrint(st.toString()); rethrow; - } finally { - await logout(session); } } Future>> downloadImages(List urls) async { - df.LoginSession? session; - - debugPrint(urls.toString()); - try { - session = await getSession(); - - final header = { - "sessionId": session.getAuthorizationHeader().$2, - "appKey": config.appNames[0], - }; - return urls.map((url) async { return (await http.get( - Uri.parse("${config.host}$url"), - headers: header, + Uri.parse("${config.backendUrl}$url"), + headers: getSessionOrThrow(), )).bodyBytes; }).toList(); } catch (e, st) { @@ -248,22 +261,22 @@ class NoteService extends ErpFrameService { debugPrint(st.toString()); rethrow; - } finally { - await logout(session); } } Future removeImage(String oid) async { - df.LoginSession? session; - try { - session = await getSession(); - df.DocuFrameMacroResponse response = await df.Macro( - config: dfConfig, - session: session, - ).execute("_web_removeImage", parameter: {"oid": oid}); + var response = await http.post( + urlBuilder("_web_removeImage"), + headers: getSessionOrThrow(), + body: {"oid": oid}, + ); - Map responseJson = jsonDecode(response.body!); + if (response.statusCode == HttpStatus.unauthorized) { + throw UserUnauthorized(); + } + + Map responseJson = jsonDecode(response.body); debugPrint(oid); debugPrint(responseJson.toString()); @@ -280,8 +293,6 @@ class NoteService extends ErpFrameService { debugPrint(st.toString()); rethrow; - } finally { - await logout(session); } } } diff --git a/lib/feature/delivery/overview/bloc/tour_bloc.dart b/lib/feature/delivery/overview/bloc/tour_bloc.dart index 434301e..7399571 100644 --- a/lib/feature/delivery/overview/bloc/tour_bloc.dart +++ b/lib/feature/delivery/overview/bloc/tour_bloc.dart @@ -1,28 +1,254 @@ +import 'package:flutter/cupertino.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:hl_lieferservice/feature/delivery/overview/bloc/tour_event.dart'; import 'package:hl_lieferservice/feature/delivery/overview/bloc/tour_state.dart'; import 'package:hl_lieferservice/feature/delivery/overview/repository/tour_repository.dart'; +import 'package:hl_lieferservice/feature/delivery/overview/service/distance_service.dart'; +import 'package:hl_lieferservice/model/delivery.dart'; import 'package:hl_lieferservice/model/tour.dart'; import 'package:hl_lieferservice/widget/operations/bloc/operation_bloc.dart'; import 'package:hl_lieferservice/widget/operations/bloc/operation_event.dart'; class TourBloc extends Bloc { OperationBloc opBloc; - TourRepository deliveryRepository; + TourRepository tourRepository; - TourBloc({required this.opBloc, required this.deliveryRepository}) + TourBloc({required this.opBloc, required this.tourRepository}) : super(TourInitial()) { on(_load); + on(_update); + on(_assignCar); + on(_increment); + on(_scan); + on(_holdDelivery); + on(_cancelDelivery); + on(_reactiveateDelivery); + } + + void _reactiveateDelivery( + ReactivateDeliveryEvent event, + Emitter emit, + ) async { + final currentState = state; + if (currentState is TourLoaded) { + opBloc.add(LoadOperation()); + try { + Tour tourCopied = currentState.tour.copyWith(); + Delivery delivery = tourCopied.deliveries.firstWhere((delivery) => delivery.id == event.deliveryId); + delivery.state = DeliveryState.ongoing; + + await tourRepository.updateDelivery( + delivery, + ); + + opBloc.add(FinishOperation()); + + emit(TourLoaded(tour: tourCopied, distances: currentState.distances)); + } catch (e, st) { + debugPrint("$e"); + debugPrint("$st"); + opBloc.add( + FailOperation(message: "Fehler beim Zurückstellen der Lieferung"), + ); + } + } + } + + void _holdDelivery( + HoldDeliveryEvent event, + Emitter emit, + ) async { + final currentState = state; + if (currentState is TourLoaded) { + opBloc.add(LoadOperation()); + try { + Tour tourCopied = currentState.tour.copyWith(); + Delivery delivery = tourCopied.deliveries.firstWhere((delivery) => delivery.id == event.deliveryId); + delivery.state = DeliveryState.onhold; + + await tourRepository.updateDelivery( + delivery, + ); + + opBloc.add(FinishOperation()); + + emit(TourLoaded(tour: tourCopied, distances: currentState.distances)); + } catch (e, st) { + debugPrint("$e"); + debugPrint("$st"); + opBloc.add( + FailOperation(message: "Fehler beim Zurückstellen der Lieferung"), + ); + } + } + } + + void _cancelDelivery(CancelDeliveryEvent event, Emitter emit) async { + final currentState = state; + if (currentState is TourLoaded) { + opBloc.add(LoadOperation()); + try { + Tour tourCopied = currentState.tour.copyWith(); + Delivery delivery = tourCopied.deliveries.firstWhere((delivery) => delivery.id == event.deliveryId); + delivery.state = DeliveryState.canceled; + + await tourRepository.updateDelivery( + delivery, + ); + + opBloc.add(FinishOperation()); + + emit(TourLoaded(tour: tourCopied, distances: currentState.distances)); + } catch (e, st) { + debugPrint("$e"); + debugPrint("$st"); + opBloc.add( + FailOperation(message: "Fehler beim Zurückstellen der Lieferung"), + ); + } + } + } + + void _scan(ScanArticleEvent event, Emitter emit) async { + final currentState = state; + opBloc.add(LoadOperation()); + + if (currentState is TourLoaded) { + try { + if (currentState.tour.deliveries.any( + (delivery) => delivery.articles.any( + (article) => article.articleNumber == event.articleNumber, + ), + )) { + var tourCopied = currentState.tour.copyWith(); + var delivery = tourCopied.deliveries.firstWhere( + (delivery) => delivery.id == event.deliveryId, + ); + var article = delivery.articles.firstWhere( + (article) => article.articleNumber == event.articleNumber, + ); + + await tourRepository.scanArticle(article.internalId.toString()); + + if (article.scannedAmount < article.amount) { + article.scannedAmount += 1; + + emit(TourLoaded(tour: tourCopied, distances: currentState.distances)); + opBloc.add(FinishOperation(message: '${article.name} gescannt')); + } else { + opBloc.add( + FailOperation( + message: 'Alle ${article.name} wurden bereits gescannt', + ), + ); + } + } else { + opBloc.add( + FailOperation( + message: 'Fehler: Artikel ist für keine Lieferung vorgesehen', + ), + ); + } + } catch (e, st) { + debugPrint(st.toString()); + opBloc.add(FailOperation(message: "Fehler beim Scannnen des Artikels")); + } + } + } + + Future _update(UpdateTour event, Emitter emit) async { + final currentState = state; + if (currentState is TourLoaded) { + emit(TourLoaded(tour: event.tour, distances: currentState.distances)); + } + } + + Future _increment( + IncrementArticleScanAmount event, + Emitter emit, + ) async { + final currentState = state; + + if (currentState is TourLoaded) { + var deliveryCopied = currentState.tour.deliveries.firstWhere( + (delivery) => delivery.id == event.deliveryId, + ); + var articleCopied = deliveryCopied.articles.firstWhere( + (article) => article.internalId == int.parse(event.internalArticleId), + ); + articleCopied.scannedAmount += 1; + + emit( + TourLoaded( + tour: currentState.tour.copyWith( + deliveries: + currentState.tour.deliveries.map((delivery) { + if (delivery.id == event.deliveryId) { + return deliveryCopied; + } + + return delivery; + }).toList(), + ), + distances: currentState.distances + ), + ); + } + } + + Future _assignCar(AssignCarEvent event, Emitter emit) async { + final currentState = state; + + if (currentState is TourLoaded) { + opBloc.add(LoadOperation()); + var copiedTour = currentState.tour.copyWith(); + var delivery = copiedTour.deliveries.firstWhere( + (delivery) => delivery.id == event.deliveryId, + ); + + try { + await tourRepository.assignCar(event.deliveryId, event.carId); + delivery.carId = int.parse(event.carId); + + emit( + TourLoaded( + tour: copiedTour.copyWith( + deliveries: + copiedTour.deliveries.map((d) { + if (d.id == delivery.id) { + return delivery; + } + + return d; + }).toList(), + ), + distances: currentState.distances + ), + ); + + opBloc.add(FinishOperation()); + } catch (e, st) { + debugPrint(st.toString()); + opBloc.add( + FailOperation(message: "Fehler beim Zuweisen des Fahrzeugs"), + ); + } + } } Future _load(LoadTour event, Emitter emit) async { opBloc.add(LoadOperation()); try { - Tour tour = await deliveryRepository.loadAll(event.teamId); - List payments = await deliveryRepository.loadPaymentOptions(); + Tour tour = await tourRepository.loadAll(event.teamId); + List payments = await tourRepository.loadPaymentOptions(); tour.paymentMethods = payments; + Map distances = {}; - emit(TourLoaded(tour: tour)); + for (final delivery in tour.deliveries) { + distances[delivery.id] = await DistanceService.getDistanceByRoad(delivery.customer.address.toString()); + } + + emit(TourLoaded(tour: tour, distances: distances)); opBloc.add(FinishOperation()); } catch (e) { opBloc.add( diff --git a/lib/feature/delivery/overview/bloc/tour_event.dart b/lib/feature/delivery/overview/bloc/tour_event.dart index b33e2cd..97b3635 100644 --- a/lib/feature/delivery/overview/bloc/tour_event.dart +++ b/lib/feature/delivery/overview/bloc/tour_event.dart @@ -1,3 +1,5 @@ +import 'package:hl_lieferservice/model/tour.dart'; + abstract class TourEvent {} class LoadTour extends TourEvent { @@ -5,3 +7,49 @@ class LoadTour extends TourEvent { LoadTour({required this.teamId}); } + +class UpdateTour extends TourEvent { + Tour tour; + + UpdateTour({required this.tour}); +} + +class AssignCarEvent extends TourEvent { + String deliveryId; + String carId; + + AssignCarEvent({required this.deliveryId, required this.carId}); +} + +class IncrementArticleScanAmount extends TourEvent { + String internalArticleId; + String deliveryId; + + IncrementArticleScanAmount({required this.internalArticleId, required this.deliveryId}); +} + +class ScanArticleEvent extends TourEvent { + ScanArticleEvent({required this.articleNumber, required this.carId, required this.deliveryId}); + + String articleNumber; + String deliveryId; + String carId; +} + +class CancelDeliveryEvent extends TourEvent { + String deliveryId; + + CancelDeliveryEvent({required this.deliveryId}); +} + +class HoldDeliveryEvent extends TourEvent { + String deliveryId; + + HoldDeliveryEvent({required this.deliveryId}); +} + +class ReactivateDeliveryEvent extends TourEvent { + String deliveryId; + + ReactivateDeliveryEvent({required this.deliveryId}); +} \ No newline at end of file diff --git a/lib/feature/delivery/overview/bloc/tour_state.dart b/lib/feature/delivery/overview/bloc/tour_state.dart index d3b0246..2983b53 100644 --- a/lib/feature/delivery/overview/bloc/tour_state.dart +++ b/lib/feature/delivery/overview/bloc/tour_state.dart @@ -8,6 +8,7 @@ class TourLoading extends TourState {} class TourLoaded extends TourState { Tour tour; + Map distances; - TourLoaded({required this.tour}); + TourLoaded({required this.tour, required this.distances}); } \ No newline at end of file diff --git a/lib/feature/delivery/overview/presentation/delivery_item.dart b/lib/feature/delivery/overview/presentation/delivery_item.dart index ecd2bfa..ce6bfc8 100644 --- a/lib/feature/delivery/overview/presentation/delivery_item.dart +++ b/lib/feature/delivery/overview/presentation/delivery_item.dart @@ -1,12 +1,12 @@ import 'package:flutter/material.dart'; import 'package:hl_lieferservice/model/delivery.dart'; - import 'package:hl_lieferservice/feature/delivery/detail/presentation/delivery_detail_page.dart'; class DeliveryListItem extends StatelessWidget { final Delivery delivery; + final double distance; - const DeliveryListItem({super.key, required this.delivery}); + const DeliveryListItem({super.key, required this.delivery, required this.distance}); Widget _leading(BuildContext context) { if (delivery.state == DeliveryState.finished) { @@ -21,7 +21,7 @@ class DeliveryListItem extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ Icon(Icons.location_on, color: Theme.of(context).primaryColor), - Text("5min"), + Text("${distance.toStringAsFixed(2)}km"), ], ); } diff --git a/lib/feature/delivery/overview/presentation/delivery_list.dart b/lib/feature/delivery/overview/presentation/delivery_list.dart index 8547932..fa4e537 100644 --- a/lib/feature/delivery/overview/presentation/delivery_list.dart +++ b/lib/feature/delivery/overview/presentation/delivery_list.dart @@ -5,8 +5,13 @@ import 'delivery_item.dart'; class DeliveryList extends StatefulWidget { final List deliveries; + final Map distances; - const DeliveryList({super.key, required this.deliveries}); + const DeliveryList({ + super.key, + required this.deliveries, + required this.distances, + }); @override State createState() => _DeliveryListState(); @@ -23,9 +28,13 @@ class _DeliveryListState extends State { return ListView.separated( separatorBuilder: (context, index) => const Divider(height: 0), - itemBuilder: - (context, index) => - DeliveryListItem(delivery: widget.deliveries[index]), + itemBuilder: (context, index) { + Delivery delivery = widget.deliveries[index]; + return DeliveryListItem( + delivery: delivery, + distance: widget.distances[delivery.id]!, + ); + }, itemCount: widget.deliveries.length, ); } diff --git a/lib/feature/delivery/overview/presentation/delivery_overview.dart b/lib/feature/delivery/overview/presentation/delivery_overview.dart index f777590..c971195 100644 --- a/lib/feature/delivery/overview/presentation/delivery_overview.dart +++ b/lib/feature/delivery/overview/presentation/delivery_overview.dart @@ -6,32 +6,48 @@ import 'package:hl_lieferservice/feature/delivery/overview/presentation/delivery import 'package:hl_lieferservice/feature/delivery/overview/presentation/delivery_list.dart'; import 'package:hl_lieferservice/model/tour.dart'; +import '../../../../model/delivery.dart'; import '../../../authentication/bloc/auth_bloc.dart'; import '../../../authentication/bloc/auth_state.dart'; class DeliveryOverview extends StatefulWidget { - const DeliveryOverview({super.key, required this.tour}); + const DeliveryOverview({super.key, required this.tour, required this.distances}); final Tour tour; + final Map distances; @override State createState() => _DeliveryOverviewState(); } class _DeliveryOverviewState extends State { - String? _selectedCarPlate; + int? _selectedCarId; + late List _deliveries; @override void initState() { super.initState(); // Select the first car for initialization - _selectedCarPlate = widget.tour.driver.cars.firstOrNull?.plate; + _selectedCarId = widget.tour.driver.cars.firstOrNull?.id; + _updateDeliveries(); + } + + @override + void didUpdateWidget(covariant DeliveryOverview oldWidget) { + super.didUpdateWidget(oldWidget); + _updateDeliveries(); + } + + void _updateDeliveries() { + _deliveries = [...widget.tour.deliveries]; } Future _loadTour() async { - Authenticated state = context.read().state as Authenticated; - context.read().add(LoadTour(teamId: state.teamId)); + Authenticated state = context + .read() + .state as Authenticated; + context.read().add(LoadTour(teamId: state.user.number)); } Widget _carSelection() { @@ -41,47 +57,58 @@ class _DeliveryOverviewState extends State { child: ListView( scrollDirection: Axis.horizontal, children: - widget.tour.driver.cars.map((car) { - Color? backgroundColor; - Color? iconColor = Theme.of(context).primaryColor; - Color? textColor; + widget.tour.driver.cars.map((car) { + Color? backgroundColor; + Color? iconColor = Theme + .of(context) + .primaryColor; + Color? textColor; - if (_selectedCarPlate == car.plate) { - backgroundColor = Theme.of(context).primaryColor; - textColor = Theme.of(context).colorScheme.onSecondary; - iconColor = Theme.of(context).colorScheme.onSecondary; - } + if (_selectedCarId == car.id) { + backgroundColor = Theme + .of(context) + .primaryColor; + textColor = Theme + .of(context) + .colorScheme + .onSecondary; + iconColor = Theme + .of(context) + .colorScheme + .onSecondary; + } - return Padding( - padding: const EdgeInsets.only(right: 8), - child: GestureDetector( - onTap: () { - setState(() { - _selectedCarPlate = car.plate; - }); - }, - child: Chip( - backgroundColor: backgroundColor, - label: Row( - children: [ - Icon(Icons.local_shipping, color: iconColor, size: 20), - Padding( - padding: const EdgeInsets.only(left: 5), - child: Text( - car.plate, - style: TextStyle(color: textColor, fontSize: 12), - ), - ), - ], + return Padding( + padding: const EdgeInsets.only(right: 8), + child: GestureDetector( + onTap: () { + setState(() { + _selectedCarId = car.id; + }); + }, + child: Chip( + backgroundColor: backgroundColor, + label: Row( + children: [ + Icon(Icons.local_shipping, color: iconColor, size: 20), + Padding( + padding: const EdgeInsets.only(left: 5), + child: Text( + car.plate, + style: TextStyle(color: textColor, fontSize: 12), + ), ), - ), + ], ), - ); - }).toList(), + ), + ), + ); + }).toList(), ), ); } + @override Widget build(BuildContext context) { return RefreshIndicator( @@ -99,11 +126,46 @@ class _DeliveryOverviewState extends State { children: [ Text( "Fahrten", - style: Theme.of(context).textTheme.headlineSmall, + style: Theme + .of(context) + .textTheme + .headlineSmall, ), ], ), - IconButton(icon: Icon(Icons.filter_list), onPressed: () {}), + PopupMenuButton( + onSelected: (String value) { + setState(() { + if (value == "name-asc") { + setState(() { + _deliveries.sort(); + }); + } + + if (value == "name-desc") { + setState(() { + _deliveries = _deliveries.reversed.toList(); + }); + } + }); + }, + itemBuilder: (BuildContext context) => >[ + PopupMenuItem( + value: 'name-asc', + child: Text('Name (A-Z)'), + ), + PopupMenuItem( + value: 'name-desc', + child: Text('Name (Z-A)'), + ), + PopupMenuItem( + value: 'distance', + child: Text('Entfernung'), + ), + ], + child: Icon(Icons.filter_list), + ) + ], ), ), @@ -111,7 +173,19 @@ class _DeliveryOverviewState extends State { padding: const EdgeInsets.only(left: 10, right: 10, bottom: 20), child: _carSelection(), ), - Expanded(child: DeliveryList(deliveries: widget.tour.deliveries)), + Expanded( + child: DeliveryList( + distances: widget.distances, + deliveries: + _deliveries + .where( + (delivery) => + delivery.carId == _selectedCarId && + delivery.allArticlesScanned(), + ) + .toList(), + ), + ), ], ), ); diff --git a/lib/feature/delivery/overview/presentation/delivery_overview_page.dart b/lib/feature/delivery/overview/presentation/delivery_overview_page.dart index b10c306..872de13 100644 --- a/lib/feature/delivery/overview/presentation/delivery_overview_page.dart +++ b/lib/feature/delivery/overview/presentation/delivery_overview_page.dart @@ -20,7 +20,7 @@ class _DeliveryOverviewPageState extends State { if (state is TourLoaded) { final currentState = state; - return Center(child: DeliveryOverview(tour: currentState.tour)); + return Center(child: DeliveryOverview(tour: currentState.tour, distances: currentState.distances)); } return Container(); diff --git a/lib/feature/delivery/overview/repository/tour_repository.dart b/lib/feature/delivery/overview/repository/tour_repository.dart index 8bd822f..0e314dd 100644 --- a/lib/feature/delivery/overview/repository/tour_repository.dart +++ b/lib/feature/delivery/overview/repository/tour_repository.dart @@ -1,4 +1,5 @@ import 'package:hl_lieferservice/feature/delivery/overview/service/delivery_info_service.dart'; +import 'package:hl_lieferservice/model/delivery.dart'; import 'package:hl_lieferservice/model/tour.dart'; class TourRepository { @@ -16,4 +17,16 @@ class TourRepository { .map((option) => Payment.fromDTO(option)) .toList(); } + + Future assignCar(String deliveryId, String carId) async { + await service.assignCar(deliveryId, carId); + } + + Future scanArticle(String internalArticleId) async { + return await service.scanArticle(internalArticleId); + } + + Future updateDelivery(Delivery delivery) { + return service.updateDelivery(delivery); + } } diff --git a/lib/feature/delivery/overview/service/delivery_info_service.dart b/lib/feature/delivery/overview/service/delivery_info_service.dart index a93084f..7f63afd 100644 --- a/lib/feature/delivery/overview/service/delivery_info_service.dart +++ b/lib/feature/delivery/overview/service/delivery_info_service.dart @@ -1,4 +1,5 @@ import 'dart:convert'; +import 'dart:io'; import 'package:docuframe/docuframe.dart' as df; import 'package:flutter/material.dart'; @@ -13,30 +14,41 @@ import 'package:hl_lieferservice/model/delivery.dart'; import 'package:hl_lieferservice/model/tour.dart'; import 'package:hl_lieferservice/util.dart'; import 'package:hl_lieferservice/services/erpframe.dart'; +import 'package:http/http.dart'; import '../../../../dto/basic_response.dart'; import '../../../../dto/discount_add_response.dart'; import '../../../../dto/discount_remove_response.dart'; import '../../../../dto/discount_update_response.dart'; import '../../../../dto/scan_response.dart'; +import '../../../authentication/exceptions.dart'; class DeliveryInfoService extends ErpFrameService { DeliveryInfoService({required super.config}); Future updateDelivery(Delivery delivery) async { - df.LoginSession? session; - try { - session = await getSession(); - df.DocuFrameMacroResponse response = - await df.Macro(config: dfConfig, session: session).execute( - "_web_updateDelivery", - parameter: DeliveryUpdateDTO.fromEntity(delivery).toJson() - as Map); + var headers = { + "Content-Type": "application/json" + }; + headers.addAll(getSessionOrThrow()); - df.Logout(config: dfConfig, session: session).logout(); + debugPrint(getSessionOrThrow().toString()); + debugPrint(jsonEncode(DeliveryUpdateDTO.fromEntity(delivery).toJson())); - Map responseJson = jsonDecode(response.body!); + + var response = await post( + urlBuilder("_web_updateDelivery"), + headers: headers, + body: jsonEncode(DeliveryUpdateDTO.fromEntity(delivery).toJson()), + ); + + if (response.statusCode == HttpStatus.unauthorized) { + throw UserUnauthorized(); + } + + debugPrint("BODY: ${response.body}"); + Map responseJson = jsonDecode(response.body); DeliveryUpdateResponseDTO responseDto = DeliveryUpdateResponseDTO.fromJson(responseJson); @@ -44,7 +56,45 @@ class DeliveryInfoService extends ErpFrameService { return; } - throw responseDto.message; + debugPrint("ERROR UPDATING:"); + debugPrint(responseDto.message); + } on df.DocuFrameException catch (e, st) { + debugPrint("ERROR WHILE UPDATING DELIVERY"); + debugPrint(e.errorMessage); + debugPrint(e.errorCode); + debugPrint(st.toString()); + + rethrow; + } + } + + Future assignCar(String deliveryId, String carId) async { + try { + var headers = { + "Content-Type": "application/json" + }; + headers.addAll(getSessionOrThrow()); + + var response = await post( + urlBuilder("_web_updateDelivery"), + headers: headers, + body: jsonEncode({"delivery_id": deliveryId, "car_id": carId}), + ); + + if (response.statusCode == HttpStatus.unauthorized) { + throw UserUnauthorized(); + } + + Map responseJson = jsonDecode(response.body); + DeliveryUpdateResponseDTO responseDto = + DeliveryUpdateResponseDTO.fromJson(responseJson); + + if (responseDto.code == "200") { + return; + } + + debugPrint("ERROR UPDATING:"); + debugPrint(responseDto.message); } on df.DocuFrameException catch (e, st) { debugPrint("ERROR WHILE UPDATING DELIVERY"); debugPrint(e.errorMessage); @@ -52,27 +102,25 @@ class DeliveryInfoService extends ErpFrameService { debugPrint(st.toString()); rethrow; - } finally { - await logout(session); } } /// List all available deliveries for today. Future getTourOfToday(String userId) async { - df.LoginSession? session; - try { - session = await getSession(); - df.DocuFrameMacroResponse response = - await df.Macro(config: dfConfig, session: session).execute( - "_web_getDeliveries", - parameter: {"driver_id": userId, "date": getTodayDate()}); - - Map responseJson = jsonDecode(response.body!); + var response = await post( + urlBuilder("_web_getDeliveries"), + headers: getSessionOrThrow(), + body: {"driver_id": userId, "date": getTodayDate()}, + ); + if (response.statusCode == HttpStatus.unauthorized) { + throw UserUnauthorized(); + } + DeliveryResponseDTO responseDto = - DeliveryResponseDTO.fromJson(responseJson); + DeliveryResponseDTO.fromJson(jsonDecode(response.body)); return Tour( discountArticleNumber: responseDto.discountArticleNumber, @@ -93,21 +141,22 @@ class DeliveryInfoService extends ErpFrameService { debugPrint("RANDOM EXCEPTION!"); rethrow; - } finally { - await logout(session); } } Future> getPaymentMethods() async { - df.LoginSession? session; - try { - session = await getSession(); - df.DocuFrameMacroResponse response = - await df.Macro(config: dfConfig, session: session) - .execute("_web_getPaymentMethods", parameter: {}); + var response = await post( + urlBuilder("_web_getPaymentMethods"), + headers: getSessionOrThrow(), + body: {}, + ); - Map responseJson = jsonDecode(response.body!); + if (response.statusCode == HttpStatus.unauthorized) { + throw UserUnauthorized(); + } + + Map responseJson = jsonDecode(response.body); PaymentMethodListDTO responseDto = PaymentMethodListDTO.fromJson(responseJson); @@ -118,30 +167,27 @@ class DeliveryInfoService extends ErpFrameService { debugPrint(st.toString()); rethrow; - } finally { - await logout(session); } } Future unscanArticle( String internalId, int amount, String reason) async { - df.LoginSession? session; - - debugPrint("AMOUNT: $amount"); - debugPrint("ID: $internalId"); - try { - session = await getSession(); - df.DocuFrameMacroResponse response = - await df.Macro(config: dfConfig, session: session) - .execute("_web_unscanArticle", parameter: { - "article_id": internalId, - "amount": amount.toString(), - "reason": reason - }); + var response = await post( + urlBuilder("_web_unscanArticle"), + headers: getSessionOrThrow(), + body: { + "article_id": internalId, + "amount": amount.toString(), + "reason": reason + }, + ); - Map responseJson = jsonDecode(response.body!); - debugPrint(responseJson.toString()); + if (response.statusCode == HttpStatus.unauthorized) { + throw UserUnauthorized(); + } + + Map responseJson = jsonDecode(response.body); ScanResponseDTO responseDto = ScanResponseDTO.fromJson(responseJson); if (responseDto.succeeded == true) { @@ -155,22 +201,22 @@ class DeliveryInfoService extends ErpFrameService { debugPrint(st.toString()); rethrow; - } finally { - await logout(session); } } Future resetScannedArticleAmount(String receiptRowId) async { - df.LoginSession? session; - try { - session = await getSession(); - df.DocuFrameMacroResponse response = - await df.Macro(config: dfConfig, session: session).execute( - "_web_unscanArticleReset", - parameter: {"receipt_row_id": receiptRowId}); + var response = await post( + urlBuilder("_web_unscanArticleReset"), + headers: getSessionOrThrow(), + body: {"receipt_row_id": receiptRowId}, + ); - Map responseJson = jsonDecode(response.body!); + if (response.statusCode == HttpStatus.unauthorized) { + throw UserUnauthorized(); + } + + Map responseJson = jsonDecode(response.body); BasicResponseDTO responseDto = BasicResponseDTO.fromJson(responseJson); if (responseDto.succeeded == true) { @@ -184,27 +230,27 @@ class DeliveryInfoService extends ErpFrameService { debugPrint(st.toString()); rethrow; - } finally { - await logout(session); } } Future addDiscount( String deliveryId, int discount, String note) async { - df.LoginSession? session; - try { - session = await getSession(); - df.DocuFrameMacroResponse response = - await df.Macro(config: dfConfig, session: session) - .execute("_web_addDiscount", parameter: { - "delivery_id": deliveryId, - "discount": discount.toString(), - "note": note - }); + var response = await post( + urlBuilder("_web_addDiscount"), + headers: getSessionOrThrow(), + body: { + "delivery_id": deliveryId, + "discount": discount.toString(), + "note": note + }, + ); - debugPrint("BODY: ${response.body!}"); - Map responseJson = jsonDecode(response.body!); + if (response.statusCode == HttpStatus.unauthorized) { + throw UserUnauthorized(); + } + + Map responseJson = jsonDecode(response.body); // let it throw, if the values are invalid return DiscountAddResponseDTO.fromJson(responseJson); @@ -214,25 +260,24 @@ class DeliveryInfoService extends ErpFrameService { debugPrint(st.toString()); rethrow; - } finally { - await logout(session); } } Future removeDiscount(String deliveryId) async { - df.LoginSession? session; - try { - session = await getSession(); - df.DocuFrameMacroResponse response = - await df.Macro(config: dfConfig, session: session) - .execute("_web_removeDiscount", parameter: { - "delivery_id": deliveryId, - }); + var response = await post( + urlBuilder("_web_removeDiscount"), + headers: getSessionOrThrow(), + body: { + "delivery_id": deliveryId, + }, + ); - debugPrint("${response.body!}"); + if (response.statusCode == HttpStatus.unauthorized) { + throw UserUnauthorized(); + } - Map responseJson = jsonDecode(response.body!); + Map responseJson = jsonDecode(response.body); // let it throw, if the values are invalid return DiscountRemoveResponseDTO.fromJson(responseJson); @@ -242,26 +287,27 @@ class DeliveryInfoService extends ErpFrameService { debugPrint(st.toString()); rethrow; - } finally { - await logout(session); } } Future updateDiscount( String deliveryId, String? note, int? discount) async { - df.LoginSession? session; - try { - session = await getSession(); - df.DocuFrameMacroResponse response = - await df.Macro(config: dfConfig, session: session) - .execute("_web_updateDiscount", parameter: { - "delivery_id": deliveryId, - "discount": discount, - "note": note - }); + var response = await post( + urlBuilder("_web_updateDiscount"), + headers: getSessionOrThrow(), + body: { + "delivery_id": deliveryId, + "discount": discount, + "note": note + }, + ); - Map responseJson = jsonDecode(response.body!); + if (response.statusCode == HttpStatus.unauthorized) { + throw UserUnauthorized(); + } + + Map responseJson = jsonDecode(response.body); // let it throw, if the values are invalid return DiscountUpdateResponseDTO.fromJson(responseJson); @@ -271,8 +317,37 @@ class DeliveryInfoService extends ErpFrameService { debugPrint(st.toString()); rethrow; - } finally { - await logout(session); + } + } + + Future scanArticle(String internalId) async { + try { + var response = await post( + urlBuilder("_web_scanArticle"), + headers: getSessionOrThrow(), + body: {"internal_id": internalId}, + ); + + debugPrint(jsonEncode({"internal_id": internalId})); + + if (response.statusCode == HttpStatus.unauthorized) { + throw UserUnauthorized(); + } + + Map responseJson = jsonDecode(response.body); + debugPrint(responseJson.toString()); + ScanResponseDTO responseDto = ScanResponseDTO.fromJson( + responseJson, + ); + + if (responseDto.succeeded == true) { + return; + } else { + debugPrint("ERROR: ${responseDto.message}"); + throw responseDto.message; + } + } catch (e) { + rethrow; } } } diff --git a/lib/feature/delivery/overview/service/discount_service.dart b/lib/feature/delivery/overview/service/discount_service.dart deleted file mode 100644 index acef14c..0000000 --- a/lib/feature/delivery/overview/service/discount_service.dart +++ /dev/null @@ -1,14 +0,0 @@ -import 'dart:convert'; - -import '../../../../services/erpframe.dart'; -import 'package:docuframe/docuframe.dart' as df; -import 'package:flutter/cupertino.dart'; - -import '../../../../dto/discount_add_response.dart'; -import '../../../../dto/discount_remove_response.dart'; -import '../../../../dto/discount_update_response.dart'; - -class DiscountService extends ErpFrameService { - DiscountService({required super.config}); - -} diff --git a/lib/feature/delivery/overview/service/distance_service.dart b/lib/feature/delivery/overview/service/distance_service.dart new file mode 100644 index 0000000..44eac69 --- /dev/null +++ b/lib/feature/delivery/overview/service/distance_service.dart @@ -0,0 +1,82 @@ +import 'package:flutter/cupertino.dart'; +import 'package:geolocator/geolocator.dart'; +import 'package:http/http.dart' as http; +import 'dart:convert'; + +class DistanceService { + static const String GOOGLE_MAPS_API_KEY = 'DEIN_API_KEY_HIER'; + + static Future getCurrentLocation() async { + bool serviceEnabled = await Geolocator.isLocationServiceEnabled(); + if (!serviceEnabled) { + throw Exception('Location services sind deaktiviert'); + } + + LocationPermission permission = await Geolocator.checkPermission(); + if (permission == LocationPermission.denied) { + permission = await Geolocator.requestPermission(); + } + + return await Geolocator.getCurrentPosition(); + } + + // Adresse in Koordinaten umwandeln (Geocoding) + static Future> getCoordinates(String address) async { + String url = + 'https://maps.googleapis.com/maps/api/geocode/json' + '?address=${Uri.encodeComponent(address)}' + '&key=AIzaSyB5_1ftLnoswoy59FzNFkrQ7SSDma5eu5E'; + + final response = await http.get(Uri.parse(url)); + + if (response.statusCode == 200) { + var json = jsonDecode(response.body); + + if (json['results'].isNotEmpty) { + var location = json['results'][0]['geometry']['location']; + return { + 'lat': location['lat'], + 'lng': location['lng'], + }; + } + throw Exception('Adresse nicht gefunden'); + } + throw Exception('Geocoding Fehler: ${response.statusCode}'); + } + + // Distanz berechnen + static Future getDistanceByRoad(String address) async { + try { + Position currentPos = await getCurrentLocation(); + Map coords = await getCoordinates(address); + + String origin = "${currentPos.latitude},${currentPos.longitude}"; + String destination = "${coords['lat']},${coords['lng']}"; + + String url = + 'https://maps.googleapis.com/maps/api/distancematrix/json' + '?origins=$origin' + '&destinations=$destination' + '&key=AIzaSyB5_1ftLnoswoy59FzNFkrQ7SSDma5eu5E'; + + final response = await http.get(Uri.parse(url)); + + debugPrint(response.body); + + if (response.statusCode == 200) { + var json = jsonDecode(response.body); + + if (json['rows'][0]['elements'][0]['status'] == 'OK') { + int distanceMeters = json['rows'][0]['elements'][0]['distance']['value']; + return distanceMeters / 1000; // In km + } else { + throw Exception('Route nicht gefunden'); + } + } else { + throw Exception('API Fehler: ${response.statusCode}'); + } + } catch (e) { + throw Exception('Fehler: $e'); + } + } +} \ No newline at end of file diff --git a/lib/feature/scan/model/article.dart b/lib/feature/scan/model/article.dart new file mode 100644 index 0000000..5ccffe9 --- /dev/null +++ b/lib/feature/scan/model/article.dart @@ -0,0 +1,21 @@ +import 'package:hl_lieferservice/model/delivery.dart'; + +class ArticleGroup { + final String articleName; + final String articleNumber; + final String internalRowId; + int totalCount; + int scannedCount; + Set deliveryIds; + + ArticleGroup({ + required this.articleName, + required this.internalRowId, + required this.articleNumber, + required this.totalCount, + required this.deliveryIds, + this.scannedCount = 0, + }); + + bool get isComplete => scannedCount >= totalCount; +} \ No newline at end of file diff --git a/lib/feature/scan/presentation/scan_article_overview.dart b/lib/feature/scan/presentation/scan_article_overview.dart new file mode 100644 index 0000000..ded09af --- /dev/null +++ b/lib/feature/scan/presentation/scan_article_overview.dart @@ -0,0 +1,94 @@ +import 'package:flutter/material.dart'; + +import '../model/article.dart'; + +class ArticleOverview extends StatefulWidget { + const ArticleOverview({super.key, required this.articleGroups}); + + final Map articleGroups; + + @override + State createState() => _ArticleOverviewState(); +} + +class _ArticleOverviewState extends State { + @override + void initState() { + super.initState(); + } + + @override + Widget build(BuildContext context) { + final sortedArticles = + widget.articleGroups.values.toList() + ..sort((a, b) => a.articleName.compareTo(b.articleName)); + + return sortedArticles.isEmpty + ? Center( + child: Text( + 'Keine Artikel zum Scannen vorhanden', + style: TextStyle(fontSize: 18), + ), + ) + : ListView.separated( + shrinkWrap: true, + physics: NeverScrollableScrollPhysics(), + itemCount: sortedArticles.length, + separatorBuilder: (context, index) => Divider(height: 0, color: Theme.of(context).colorScheme.surfaceContainerHighest), + itemBuilder: (context, index) { + final group = sortedArticles[index]; + + return ListTile( + leading: + group.isComplete + ? Icon(Icons.check_circle, color: Colors.green, size: 32) + : Container( + width: 32, + alignment: Alignment.center, + child: Text( + '${group.scannedCount}/${group.totalCount}', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: + group.scannedCount > 0 + ? Colors.blue + : Colors.grey, + ), + ), + ), + title: Text( + "${group.articleName} (Artikelnr. ${group.articleNumber})", + style: TextStyle(fontWeight: FontWeight.w500), + ), + subtitle: Padding( + padding: const EdgeInsets.only(top: 10, bottom: 10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: + group.deliveryIds + .map( + (delivery) => Row( + children: [ + Icon(Icons.person), + Padding( + padding: const EdgeInsets.only(left: 5, bottom: 10), + child: Text( + "${delivery.customer.name.toString()}\n${delivery.customer.address.toString()}", + ), + ), + ], + ), + ) + .toList(), + ), + ), + tileColor: + group.isComplete + ? Colors.green.withValues(alpha: 0.1) + : Theme.of(context).colorScheme.onSecondary, + ); + }, + ); + } +} diff --git a/lib/feature/scan/presentation/scan_page.dart b/lib/feature/scan/presentation/scan_page.dart new file mode 100644 index 0000000..c7fe549 --- /dev/null +++ b/lib/feature/scan/presentation/scan_page.dart @@ -0,0 +1,279 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:mobile_scanner/mobile_scanner.dart'; + +import 'package:hl_lieferservice/feature/delivery/overview/bloc/tour_bloc.dart'; +import 'package:hl_lieferservice/feature/delivery/overview/bloc/tour_state.dart'; +import 'package:hl_lieferservice/feature/scan/presentation/scan_screen.dart'; +import 'package:hl_lieferservice/model/tour.dart'; +import 'package:hl_lieferservice/widget/home/bloc/navigation_bloc.dart'; +import 'package:hl_lieferservice/widget/home/bloc/navigation_event.dart'; + +enum TourHomeSteps { planning, delivery, off } + +class ScanPage extends StatefulWidget { + const ScanPage({super.key}); + + @override + State createState() => _ScanPageState(); +} + +class _ScanPageState extends State { + int _currentStepIndex = 0; + + @override + void initState() { + super.initState(); + + _tryFinish(context + .read() + .state); + } + + void _onStartScan() { + Navigator.of( + context, + ).push(MaterialPageRoute(builder: (context) => ArticleScanningScreen())); + } + + Widget _tourSteps(Tour tour) { + var allArticlesScanned = tour.deliveries.every( + (delivery) => delivery.allArticlesScanned(), + ); + + return Stepper( + currentStep: _currentStepIndex, + controlsBuilder: (context, details) { + if (details.stepIndex == TourHomeSteps.planning.index) { + return Container( + alignment: Alignment.center, + child: Padding( + padding: const EdgeInsets.only(top: 15), + child: FilledButton.icon( + label: const Text("Scannen"), + onPressed: _onStartScan, + icon: const Icon(Icons.qr_code), + ), + ), + ); + } else { + return Container( + alignment: Alignment.center, + child: Padding( + padding: const EdgeInsets.only(top: 15), + child: FilledButton.icon( + label: const Text("Auslieferung starten"), + onPressed: + allArticlesScanned + ? () => + context.read().add( + NavigateToIndex(index: 1)) + : null, + icon: const Icon(Icons.local_shipping), + ), + ), + ); + } + }, + onStepContinue: + _currentStepIndex >= 1 + ? null + : () => + setState(() { + if (_currentStepIndex < 2) { + _currentStepIndex += 1; + } + }), + onStepCancel: + _currentStepIndex == 0 + ? null + : () => + setState(() { + if (_currentStepIndex > 0) { + _currentStepIndex -= 1; + } + }), + onStepTapped: + (value) => + setState(() { + if (_currentStepIndex == 1 && allArticlesScanned) { + return; + } + + _currentStepIndex = value; + }), + steps: [ + Step( + title: Row( + children: [ + Text( + "Fahrzeuge beladen", + style: TextStyle( + color: allArticlesScanned ? Colors.grey : null, + ), + ), + Padding( + padding: const EdgeInsets.only(left: 5), + child: + !allArticlesScanned + ? const Icon( + Icons.access_time_filled, + color: Colors.orangeAccent, + ) + : const Icon( + Icons.check_circle, + color: Colors.lightGreen, + ), + ), + ], + ), + content: const Column( + children: [ + Padding( + padding: EdgeInsets.only(bottom: 10), + child: Icon(Icons.barcode_reader, color: Colors.black), + ), + Text( + "Scannen Sie die Ware, die Sie für die Auslieferungen benötigen.", + ), + ], + ), + ), + Step( + title: const Text("Ausliefern"), + content: Container( + alignment: Alignment.centerLeft, + child: + !allArticlesScanned + ? const Text( + "Scannen Sie erst die benötigte Ware, um die Auslieferungen zu beginnen.", + ) + : null, + ), + ), + ], + ); + } + + Widget _info(Tour tour) { + int amountArticles = tour.deliveries.fold( + 0, + (acc, delivery) => + acc + + delivery.articles + .where((article) => article.scannable) + .fold( + 0, + (amountArticles, article) => amountArticles + article.amount, + ), + ); + + int amountCars = tour.driver.cars.length; + int amountDeliveries = tour.deliveries.length; + + return Padding( + padding: const EdgeInsets.all(10), + child: SizedBox( + width: double.infinity, + child: Card( + color: Theme + .of(context) + .colorScheme + .onSecondary, + child: Padding( + padding: const EdgeInsets.all(20), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Padding( + padding: const EdgeInsets.only(top: 0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Icon(Icons.archive), + Padding( + padding: const EdgeInsets.only(left: 5), + child: Text("Anzahl Artikel"), + ), + ], + ), + Text(amountArticles.toString()), + ], + ), + ), + + Padding( + padding: const EdgeInsets.only(top: 15), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Icon(Icons.local_shipping_outlined), + Padding( + padding: const EdgeInsets.only(left: 5), + child: Text("Anzahl Fahrzeuge"), + ), + ], + ), + Text(amountCars.toString()), + ], + ), + ), + + Padding( + padding: const EdgeInsets.only(top: 15), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Icon(Icons.person), + Padding( + padding: const EdgeInsets.only(left: 5), + child: Text("Anzahl Lieferungen"), + ), + ], + ), + Text(amountDeliveries.toString()), + ], + ), + ), + ], + ), + ), + ), + ), + ); + } + + void _tryFinish(TourState state) { + if (state is TourLoaded) { + if (state.tour.deliveries.every( + (delivery) => delivery.allArticlesScanned(), + )) { + setState(() { + _currentStepIndex = 1; + }); + } + } + } + + @override + Widget build(BuildContext context) { + return BlocConsumer( + listener: (context, state) { + _tryFinish(state); + }, + builder: (context, state) { + if (state is TourLoaded) { + return Column(children: [_info(state.tour), _tourSteps(state.tour)]); + } + + return Center(child: CircularProgressIndicator()); + }, + ); + } +} \ No newline at end of file diff --git a/lib/feature/scan/presentation/scan_screen.dart b/lib/feature/scan/presentation/scan_screen.dart new file mode 100644 index 0000000..c5c9c46 --- /dev/null +++ b/lib/feature/scan/presentation/scan_screen.dart @@ -0,0 +1,405 @@ +import 'dart:async'; +import 'package:collection/collection.dart'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:hl_lieferservice/feature/delivery/overview/bloc/tour_event.dart'; +import 'package:hl_lieferservice/feature/delivery/overview/bloc/tour_state.dart'; +import 'package:hl_lieferservice/feature/scan/presentation/scanner.dart'; +import 'package:hl_lieferservice/feature/settings/bloc/settings_bloc.dart'; +import 'package:hl_lieferservice/feature/settings/bloc/settings_state.dart'; +import 'package:hl_lieferservice/model/article.dart'; +import 'package:hl_lieferservice/model/car.dart'; +import 'package:hl_lieferservice/model/delivery.dart'; +import 'package:hl_lieferservice/model/tour.dart'; +import 'package:hl_lieferservice/widget/operations/bloc/operation_bloc.dart'; +import 'package:hl_lieferservice/widget/operations/bloc/operation_event.dart'; + +import '../../delivery/overview/bloc/tour_bloc.dart'; + +class ArticleScanningScreen extends StatefulWidget { + const ArticleScanningScreen({super.key}); + + @override + State createState() => _ArticleScanningScreenState(); +} + +class _ArticleScanningScreenState extends State { + final FocusNode _focusNode = FocusNode(); + String _buffer = ''; + Timer? _bufferTimer; + int _selectedDelivery = 0; + int? _selectedCarId; + + @override + void initState() { + super.initState(); + // Focus anfordern, um Keyboard-Events zu empfangen + WidgetsBinding.instance.addPostFrameCallback((_) { + _focusNode.requestFocus(); + }); + + final state = context.read().state; + + if (state is TourLoaded) { + setState(() { + _selectedCarId = state.tour.deliveries[_selectedDelivery].carId; + }); + } + } + + @override + void dispose() { + _focusNode.dispose(); + _bufferTimer?.cancel(); + super.dispose(); + } + + void _handleKey(KeyEvent event) { + if (event is KeyDownEvent) { + if (event.logicalKey == LogicalKeyboardKey.enter) { + // Enter = Scan abgeschlossen + _bufferTimer?.cancel(); + if (_buffer.isNotEmpty) { + _handleBarcodeScanned(_buffer); + _buffer = ''; + } + } else { + // Zeichen zum Buffer hinzufügen + final character = event.character; + if (character != null && character.isNotEmpty) { + _buffer += character; + + // Timer zurücksetzen + _bufferTimer?.cancel(); + _bufferTimer = Timer(Duration(milliseconds: 1000), () { + // Nach 1 Sekunde ohne neue Eingabe: Buffer verarbeiten + if (_buffer.isNotEmpty) { + _handleBarcodeScanned(_buffer); + _buffer = ''; + } + }); + } + } + } + } + + void _handleBarcodeScanned(String barcode) { + if (_selectedCarId == null) { + context.read().add( + FailOperation(message: "Wählen Sie zu erst ein Fahrzeug aus"), + ); + return; + } + + final state = context.read().state as TourLoaded; + + context.read().add( + ScanArticleEvent( + articleNumber: barcode, + carId: _selectedCarId!.toString(), + deliveryId: state.tour.deliveries[_selectedDelivery].id, + ), + ); + } + + Widget _carSelection(List cars, List deliveries) { + return Padding( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Fahrzeug auswählen", + style: Theme.of(context).textTheme.headlineSmall, + ), + Padding( + padding: const EdgeInsets.only(top: 10), + child: SizedBox( + width: double.infinity, + height: 50, + child: ListView( + scrollDirection: Axis.horizontal, + children: + cars.map((car) { + Color? backgroundColor; + Color? iconColor = Theme.of(context).primaryColor; + Color? textColor; + + if (_selectedCarId == car.id) { + backgroundColor = Theme.of(context).primaryColor; + textColor = Theme.of(context).colorScheme.onSecondary; + iconColor = Theme.of(context).colorScheme.onSecondary; + } + + return Padding( + padding: const EdgeInsets.only(right: 8), + child: GestureDetector( + onTap: () { + context.read().add( + AssignCarEvent( + deliveryId: deliveries[_selectedDelivery].id, + carId: car.id.toString(), + ), + ); + + setState(() { + _selectedCarId = car.id; + }); + }, + child: Chip( + backgroundColor: backgroundColor, + label: Row( + children: [ + Icon( + Icons.local_shipping, + color: iconColor, + size: 20, + ), + Padding( + padding: const EdgeInsets.only(left: 5), + child: Text( + car.plate, + style: TextStyle( + color: textColor, + fontSize: 12, + ), + ), + ), + ], + ), + ), + ), + ); + }).toList(), + ), + ), + ), + ], + ), + ); + } + + Widget _articles(List
articles) { + List
scannableArticles = + articles.where((article) => article.scannable).toList(); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(left: 20, bottom: 20), + child: Text( + "Artikel", + style: Theme.of(context).textTheme.headlineSmall, + ), + ), + scannableArticles.isEmpty + ? Center( + child: Text( + 'Keine Artikel zum Scannen vorhanden', + style: TextStyle(fontSize: 18), + ), + ) + : ListView.separated( + shrinkWrap: true, + physics: NeverScrollableScrollPhysics(), + itemCount: scannableArticles.length, + separatorBuilder: + (context, index) => Divider( + height: 0, + color: + Theme.of(context).colorScheme.surfaceContainerHighest, + ), + itemBuilder: (context, index) { + final article = scannableArticles[index]; + + return ListTile( + leading: + article.scannedAmount == article.amount + ? Icon( + Icons.check_circle, + color: Colors.green, + size: 32, + ) + : Container( + width: 32, + alignment: Alignment.center, + child: Text( + '${article.scannedAmount}/${article.amount}', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: + article.scannedAmount > 0 + ? Colors.blue + : Colors.grey, + ), + ), + ), + title: Text( + article.name, + style: TextStyle(fontWeight: FontWeight.w500), + ), + subtitle: Text("Artikelnr. ${article.articleNumber}"), + tileColor: + article.scannedAmount == article.amount + ? Colors.green.withValues(alpha: 0.1) + : Theme.of(context).colorScheme.onSecondary, + ); + }, + ), + ], + ); + } + + void _selectDelivery(int? index) { + setState(() { + _selectedDelivery = index!; + }); + } + + Widget _navigation(List deliveries) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + OutlinedButton( + onPressed: + _selectedDelivery > 0 + ? () => { + if (_selectedDelivery > 0) + { + setState(() { + _selectedDelivery -= 1; + _selectedCarId = deliveries[_selectedDelivery].carId; + }), + }, + } + : null, + child: Text("zurück"), + ), + Expanded( + child: Padding( + padding: const EdgeInsets.only(left: 20, right: 20), + child: DropdownButton( + menuWidth: MediaQuery.of(context).size.width, + isExpanded: true, + items: + deliveries + .mapIndexed( + (index, delivery) => DropdownMenuItem( + value: index, + child: Text( + delivery.customer.name, + overflow: TextOverflow.ellipsis, + ), + ), + ) + .toList(), + onChanged: _selectDelivery, + value: _selectedDelivery, + ), + ), + ), + OutlinedButton( + onPressed: + _selectedDelivery < deliveries.length - 1 + ? () => { + if (_selectedDelivery + 1 < deliveries.length) + { + setState(() { + _selectedDelivery += 1; + _selectedCarId = deliveries[_selectedDelivery].carId; + }), + }, + } + : null, + child: Text("weiter"), + ), + ], + ); + } + + Widget _deliveryStepper(Tour tour) { + final settingsState = context.read().state; + Widget scannerWidget = BarcodeScannerWidget( + onBarcodeDetected: _handleBarcodeScanned, + ); + + if (settingsState is AppSettingsFailed) { + context.read().add( + FailOperation( + message: + "Einstellungen konnten nicht geladen werden. Nutze Kamera-Scanner.", + ), + ); + } + + if (settingsState is AppSettingsLoaded) { + if (settingsState.settings.useHardwareScanner) { + scannerWidget = Container(); + } + } + + return Padding( + padding: const EdgeInsets.all(0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + scannerWidget, + _carSelection(tour.driver.cars, tour.deliveries), + _articles(tour.deliveries[_selectedDelivery].articles), + ], + ), + ); + } + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + if (state is TourLoaded) { + Delivery delivery = state.tour.deliveries[_selectedDelivery]; + + return Scaffold( + appBar: AppBar( + title: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + delivery.customer.name, + style: TextStyle( + color: Theme.of(context).colorScheme.onSecondary, + fontWeight: FontWeight.w500, + ), + ), + Text( + delivery.customer.address.toString(), + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.normal, + color: Theme.of(context).colorScheme.onSecondary, + ), + ), + ], + ), + backgroundColor: Theme.of(context).primaryColor, + ), + bottomNavigationBar: Padding( + padding: const EdgeInsets.all(25), + child: _navigation(state.tour.deliveries), + ), + body: KeyboardListener( + focusNode: _focusNode, + onKeyEvent: _handleKey, + child: _deliveryStepper(state.tour), + ), + ); + } + + return Container(); + }, + ); + } +} diff --git a/lib/feature/scan/presentation/scanner.dart b/lib/feature/scan/presentation/scanner.dart new file mode 100644 index 0000000..c4dff23 --- /dev/null +++ b/lib/feature/scan/presentation/scanner.dart @@ -0,0 +1,80 @@ +import 'package:flutter/material.dart'; +import 'package:mobile_scanner/mobile_scanner.dart'; + +/// StatefulWidget für den Barcode-Scanner mit grünem Border-Feedback +class BarcodeScannerWidget extends StatefulWidget { + final Function(String) onBarcodeDetected; + + const BarcodeScannerWidget({ + Key? key, + required this.onBarcodeDetected, + }) : super(key: key); + + @override + State createState() => _BarcodeScannerWidgetState(); +} + +class _BarcodeScannerWidgetState extends State { + bool _isDetected = false; + DateTime? _lastScannedTime; + final Duration _scanTimeout = const Duration(milliseconds: 2000); // 2 Sekunden Cooldown + + void _handleBarcodeDetected(String barcode) { + final now = DateTime.now(); + + // Prüfe ob genug Zeit seit dem letzten erfolgreichen Scan vergangen ist + if (_lastScannedTime != null && + now.difference(_lastScannedTime!).inMilliseconds < _scanTimeout.inMilliseconds) { + // Timeout nicht abgelaufen - ignoriere diesen Scan + debugPrint('Scan ignoriert - Cooldown aktiv'); + return; + } + + // Update letzte Scan-Zeit + _lastScannedTime = now; + + // Rand grün färben + setState(() { + _isDetected = true; + }); + + // Nach 500ms wieder zurücksetzen + Future.delayed(const Duration(milliseconds: 500), () { + if (mounted) { + setState(() { + _isDetected = false; + }); + } + }); + + // Callback aufrufen + widget.onBarcodeDetected(barcode); + } + + @override + Widget build(BuildContext context) { + final screenHeight = MediaQuery.of(context).size.height; + final scannerHeight = screenHeight / 4; + + return Container( + height: scannerHeight, + decoration: BoxDecoration( + border: Border.all( + color: _isDetected ? Colors.green : Colors.grey, + width: _isDetected ? 4 : 2, + ), + ), + child: MobileScanner( + onDetect: (capture) { + final List barcodes = capture.barcodes; + + for (final barcode in barcodes) { + if (barcode.rawValue != null) { + _handleBarcodeDetected(barcode.rawValue!); + } + } + }, + ), + ); + } +} \ No newline at end of file diff --git a/lib/feature/scan/repository/scan_repository.dart b/lib/feature/scan/repository/scan_repository.dart new file mode 100644 index 0000000..b2880b6 --- /dev/null +++ b/lib/feature/scan/repository/scan_repository.dart @@ -0,0 +1,11 @@ +import 'package:hl_lieferservice/feature/scan/service/scan_service.dart'; + +class ScanRepository { + ScanService service; + + ScanRepository({required this.service}); + + Future scanArticle(String internalArticleId) async { + return await service.scanArticle(internalArticleId); + } +} diff --git a/lib/feature/scan/service/scan_service.dart b/lib/feature/scan/service/scan_service.dart new file mode 100644 index 0000000..d6c59ba --- /dev/null +++ b/lib/feature/scan/service/scan_service.dart @@ -0,0 +1,41 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:hl_lieferservice/dto/scan_response.dart'; +import 'package:http/http.dart' as http; +import '../../../util.dart'; +import '../../authentication/exceptions.dart'; + +class ScanService { + Future scanArticle(String internalId) async { + try { + var response = await http.post( + urlBuilder("_web_scanArticle"), + headers: getSessionOrThrow(), + body: {"internal_id": internalId}, + ); + + debugPrint(jsonEncode({"internal_id": internalId})); + + if (response.statusCode == HttpStatus.unauthorized) { + throw UserUnauthorized(); + } + + Map responseJson = jsonDecode(response.body); + debugPrint(responseJson.toString()); + ScanResponseDTO responseDto = ScanResponseDTO.fromJson( + responseJson, + ); + + if (responseDto.succeeded == true) { + return; + } else { + debugPrint("ERROR: ${responseDto.message}"); + throw responseDto.message; + } + } catch (e) { + rethrow; + } + } +} \ No newline at end of file diff --git a/lib/feature/scan/util.dart b/lib/feature/scan/util.dart new file mode 100644 index 0000000..007edab --- /dev/null +++ b/lib/feature/scan/util.dart @@ -0,0 +1,35 @@ +import 'package:hl_lieferservice/feature/scan/model/article.dart'; + +import '../../model/delivery.dart'; + +Map initializeArticleGroups(List deliveries) { + Map articleGroups = {}; + + // Alle Artikel aus allen Lieferungen durchgehen + for (var delivery in deliveries) { + for (var article in delivery.articles) { + if (articleGroups.containsKey(article.articleNumber)) { + // Artikel bereits vorhanden, Anzahl erhöhen + if (article.scannable) { + articleGroups[article.articleNumber]!.scannedCount += article.scannedAmount; + articleGroups[article.articleNumber]!.totalCount += article.amount; + articleGroups[article.articleNumber]!.deliveryIds.add(delivery); + } + } else { + if (article.scannable) { + // Neuer Artikel, hinzufügen + articleGroups[article.articleNumber] = ArticleGroup( + deliveryIds: {delivery}, + articleName: article.name, + articleNumber: article.articleNumber, + scannedCount: article.scannedAmount, + internalRowId: article.internalId.toString(), + totalCount: article.amount, + ); + } + } + } + } + + return articleGroups; +} \ No newline at end of file diff --git a/lib/feature/settings/bloc/settings_bloc.dart b/lib/feature/settings/bloc/settings_bloc.dart new file mode 100644 index 0000000..cfaed8d --- /dev/null +++ b/lib/feature/settings/bloc/settings_bloc.dart @@ -0,0 +1,40 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter_bloc/flutter_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/feature/settings/repository/settings_repository.dart'; + +class SettingsBloc extends Bloc { + SettingsRepository repository; + + SettingsBloc() + : repository = SettingsRepository(), + super(AppSettingsInitial()) { + on(_load); + on(_update); + } + + void _load(LoadSettings event, Emitter emit) async { + try { + Settings settings = await repository.getSettings(); + debugPrint("use ${settings.useHardwareScanner}"); + emit(AppSettingsLoaded(settings: settings)); + } catch (e, st) { + debugPrint("Failed to load settings: $e}"); + debugPrint("Stacktrace: ${st.toString()}"); + emit(AppSettingsFailed()); + } + } + + void _update(UpdateSettings event, Emitter emit) { + try { + repository.saveSettings(event.settings); + emit(AppSettingsLoaded(settings: event.settings.copyWith())); + } catch (e, st) { + debugPrint("Failed to save settings: $e}"); + debugPrint("Stacktrace: ${st.toString()}"); + emit(AppSettingsFailed()); + } + } +} diff --git a/lib/feature/settings/bloc/settings_event.dart b/lib/feature/settings/bloc/settings_event.dart new file mode 100644 index 0000000..98da767 --- /dev/null +++ b/lib/feature/settings/bloc/settings_event.dart @@ -0,0 +1,10 @@ +import 'package:hl_lieferservice/feature/settings/model/settings.dart'; + +abstract class SettingsEvent {} + +class UpdateSettings extends SettingsEvent { + UpdateSettings({required this.settings}); + + Settings settings; +} +class LoadSettings extends SettingsEvent {} \ No newline at end of file diff --git a/lib/feature/settings/bloc/settings_state.dart b/lib/feature/settings/bloc/settings_state.dart new file mode 100644 index 0000000..50e871e --- /dev/null +++ b/lib/feature/settings/bloc/settings_state.dart @@ -0,0 +1,11 @@ +import 'package:hl_lieferservice/feature/settings/model/settings.dart'; + +abstract class SettingsState {} + +class AppSettingsInitial extends SettingsState {} +class AppSettingsFailed extends SettingsState {} +class AppSettingsLoaded extends SettingsState { + AppSettingsLoaded({required this.settings}); + + Settings settings; +} diff --git a/lib/feature/settings/model/settings.dart b/lib/feature/settings/model/settings.dart new file mode 100644 index 0000000..f7b8bf7 --- /dev/null +++ b/lib/feature/settings/model/settings.dart @@ -0,0 +1,10 @@ +class Settings { + Settings({required this.useHardwareScanner}); + + bool useHardwareScanner; + + Settings copyWith({bool? useHardwareScanner}) { + return Settings( + useHardwareScanner: useHardwareScanner ?? this.useHardwareScanner); + } +} \ No newline at end of file diff --git a/lib/feature/settings/presentation/settings_page.dart b/lib/feature/settings/presentation/settings_page.dart new file mode 100644 index 0000000..89d6cb7 --- /dev/null +++ b/lib/feature/settings/presentation/settings_page.dart @@ -0,0 +1,132 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.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'; + +class SettingsPage extends StatefulWidget { + const SettingsPage({super.key}); + + @override + State createState() => _SettingsPage(); +} + +class _SettingsPage extends State { + void _logout() {} + + void _changePassword() {} + + Widget _scanSettings() { + return BlocBuilder( + 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().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("Passwort öndern"), + trailing: Padding( + padding: const EdgeInsets.all(2), + child: IconButton( + onPressed: _changePassword, + icon: FilledButton( + onPressed: _changePassword, + child: const Text("Ändern"), + ), + ), + ), + tileColor: Theme.of(context).colorScheme.onSecondary, + ), + + ListTile( + title: const Text("Ausloggen"), + trailing: IconButton( + onPressed: _logout, + icon: Icon(Icons.logout, color: Colors.redAccent), + ), + tileColor: Theme.of(context).colorScheme.onSecondary, + ), + ], + ), + appBar: AppBar( + title: Text( + "Einstellungen", + style: Theme.of(context).textTheme.headlineMedium, + ), + ), + ); + } +} diff --git a/lib/feature/settings/repository/settings_repository.dart b/lib/feature/settings/repository/settings_repository.dart new file mode 100644 index 0000000..2e7a768 --- /dev/null +++ b/lib/feature/settings/repository/settings_repository.dart @@ -0,0 +1,22 @@ +import 'package:hl_lieferservice/feature/settings/model/settings.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class SettingsRepository { + Future getSettings() async { + final prefs = await SharedPreferences.getInstance(); + + bool? useHardwareScanner = prefs.getBool("useHardwareScanner"); + + if (useHardwareScanner == null) { + await prefs.setBool("useHardwareScanner", false); + useHardwareScanner = false; + } + + return Settings(useHardwareScanner: useHardwareScanner); + } + + Future saveSettings(Settings settings) async { + final prefs = await SharedPreferences.getInstance(); + await prefs.setBool("useHardwareScanner", settings.useHardwareScanner); + } +} \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 62ba196..ecba437 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,11 +1,19 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:get_it/get_it.dart'; import 'package:hl_lieferservice/bloc/app_bloc.dart'; import 'package:hl_lieferservice/bloc/app_events.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/widget/app.dart'; +final locator = GetIt.instance; + void main() { - runApp(BlocProvider(create: (context) => AppBloc(), child: App())); + runApp(MultiBlocProvider(providers: [ + BlocProvider(create: (context) => AppBloc(),), + BlocProvider(create: (context) => SettingsBloc()) + ], child: App())); } class App extends StatefulWidget { @@ -20,11 +28,11 @@ class _AppState extends State { void initState() { super.initState(); context.read().add(AppLoadConfig(path: "hl_server_config.json")); + context.read().add(LoadSettings()); } @override Widget build(BuildContext context) { return DeliveryApp(); } -} - +} \ No newline at end of file diff --git a/lib/model/delivery.dart b/lib/model/delivery.dart index dbf5e8d..2b09230 100644 --- a/lib/model/delivery.dart +++ b/lib/model/delivery.dart @@ -135,7 +135,7 @@ class DeliveryOption { if (value.isEmpty) { return false; } else { - return bool.parse(value); + return value == "0" ? false : true; } } else { if (value.isEmpty) { @@ -161,7 +161,7 @@ class DeliveryOption { } } -class Delivery { +class Delivery implements Comparable { Delivery({ required this.customer, required this.id, @@ -208,6 +208,11 @@ class Delivery { Payment payment; List options; + @override + int compareTo(Delivery other) { + return customer.name.compareTo(other.customer.name); + } + Delivery copyWith({ Customer? customer, String? id, diff --git a/lib/model/tour.dart b/lib/model/tour.dart index 4c957d9..86556af 100644 --- a/lib/model/tour.dart +++ b/lib/model/tour.dart @@ -45,6 +45,22 @@ class Tour { .toList() .length; } + + Tour copyWith({ + DateTime? date, + String? discountArticleNumber, + Driver? driver, + List? deliveries, + List? paymentMethods, + }) { + return Tour( + date: date ?? this.date, + discountArticleNumber: discountArticleNumber ?? this.discountArticleNumber, + driver: driver ?? this.driver, + deliveries: deliveries ?? this.deliveries, + paymentMethods: paymentMethods ?? this.paymentMethods, + ); + } } class Driver { diff --git a/lib/services/auth_service.dart b/lib/services/auth_service.dart deleted file mode 100644 index e69de29..0000000 diff --git a/lib/services/erpframe.dart b/lib/services/erpframe.dart index 2f832f9..8e7a807 100644 --- a/lib/services/erpframe.dart +++ b/lib/services/erpframe.dart @@ -5,6 +5,7 @@ import 'package:flutter/cupertino.dart'; class LocalDocuFrameConfiguration { String host; + String backendUrl; final String user; final String pass; final List appNames; @@ -15,6 +16,7 @@ class LocalDocuFrameConfiguration { required this.appKey, required this.appNames, required this.pass, + required this.backendUrl, required this.user}); Map toJson() { @@ -22,6 +24,7 @@ class LocalDocuFrameConfiguration { "host": host, "user": user, "pass": pass, + "backendUrl": backendUrl, "appNames": appNames, "appKey": appKey }; @@ -34,6 +37,7 @@ class LocalDocuFrameConfiguration { appNames: (getValueOrThrowIfNotPresent("appNames", json) as List) .cast(), pass: getValueOrThrowIfNotPresent("pass", json), + backendUrl: getValueOrThrowIfNotPresent("backendUrl", json), user: getValueOrThrowIfNotPresent("user", json)); } } diff --git a/lib/util.dart b/lib/util.dart index 3b639ab..fcc1d59 100644 --- a/lib/util.dart +++ b/lib/util.dart @@ -1,3 +1,8 @@ +import 'package:hl_lieferservice/exceptions.dart'; +import 'package:hl_lieferservice/feature/authentication/bloc/auth_state.dart'; +import 'package:hl_lieferservice/feature/authentication/exceptions.dart'; +import 'package:hl_lieferservice/main.dart'; +import 'package:hl_lieferservice/services/erpframe.dart'; import 'package:intl/intl.dart'; import 'model/delivery.dart'; @@ -58,3 +63,24 @@ String getName(DeliveryState state) { return "ausgeliefert"; } } + +Map getSessionOrThrow() { + if (locator.isRegistered()) { + return {"Cookie": "session_id=${locator.get().sessionId}"}; + } else { + throw UserUnauthorized(); + } +} + +LocalDocuFrameConfiguration getConfig() { + if (locator.isRegistered()) { + return locator.get(); + } else { + throw AppConfigNotFound(); + } +} + +Uri urlBuilder(String path) { + LocalDocuFrameConfiguration config = getConfig(); + return Uri.parse("${config.backendUrl}/v1/execute/$path"); +} \ No newline at end of file diff --git a/lib/widget/app.dart b/lib/widget/app.dart index 5a95237..7ba886e 100644 --- a/lib/widget/app.dart +++ b/lib/widget/app.dart @@ -3,6 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:hl_lieferservice/bloc/app_bloc.dart'; import 'package:hl_lieferservice/feature/authentication/bloc/auth_bloc.dart'; import 'package:hl_lieferservice/feature/authentication/presentation/login_enforcer.dart'; +import 'package:hl_lieferservice/feature/authentication/service/userinfo.dart'; import 'package:hl_lieferservice/feature/cars/presentation/car_management_page.dart'; import 'package:hl_lieferservice/feature/delivery/detail/bloc/delivery_bloc.dart'; import 'package:hl_lieferservice/feature/delivery/detail/bloc/note_bloc.dart'; @@ -11,14 +12,13 @@ import 'package:hl_lieferservice/feature/delivery/detail/repository/note_reposit import 'package:hl_lieferservice/feature/delivery/detail/service/notes_service.dart'; import 'package:hl_lieferservice/feature/delivery/overview/bloc/tour_bloc.dart'; import 'package:hl_lieferservice/feature/delivery/overview/repository/tour_repository.dart'; -import 'package:hl_lieferservice/repository/user_repository.dart'; +import 'package:hl_lieferservice/feature/settings/bloc/settings_bloc.dart'; import 'package:hl_lieferservice/widget/home/bloc/navigation_bloc.dart'; import 'package:hl_lieferservice/widget/operations/bloc/operation_bloc.dart'; import 'package:hl_lieferservice/widget/operations/presentation/operation_view_enforcer.dart'; import 'package:hl_lieferservice/bloc/app_states.dart'; import '../feature/delivery/overview/service/delivery_info_service.dart'; -import 'home/bloc/navigation_state.dart'; import 'home/presentation/home.dart'; class DeliveryApp extends StatefulWidget { @@ -42,7 +42,9 @@ class _DeliveryAppState extends State { BlocProvider( create: (context) => AuthBloc( - repository: UserRepository(), + service: UserInfoService( + url: currentAppState.config.backendUrl, + ), operationBloc: context.read(), ), ), @@ -50,7 +52,7 @@ class _DeliveryAppState extends State { create: (context) => TourBloc( opBloc: context.read(), - deliveryRepository: TourRepository( + tourRepository: TourRepository( service: DeliveryInfoService( config: currentAppState.config, ), @@ -70,6 +72,9 @@ class _DeliveryAppState extends State { create: (context) => DeliveryBloc( opBloc: context.read(), + noteRepository: NoteRepository( + service: NoteService(config: currentAppState.config), + ), repository: DeliveryRepository( service: DeliveryInfoService( config: currentAppState.config, @@ -93,20 +98,7 @@ class _DeliveryAppState extends State { } if (state is AppConfigLoaded) { - return BlocConsumer( - listener: (context, state) { - if (state is NavigateToRoute) { - Navigator.pushNamed( - context, - state.routeName, - arguments: state.arguments, - ); - } - }, - builder: (BuildContext context, NavigationState state) { - return LoginEnforcer(child: Home()); - }, - ); + return LoginEnforcer(child: Home()); } return Container(); diff --git a/lib/widget/app_bar.dart b/lib/widget/app_bar.dart new file mode 100644 index 0000000..b75010e --- /dev/null +++ b/lib/widget/app_bar.dart @@ -0,0 +1,25 @@ +import 'package:flutter/material.dart'; +import 'package:hl_lieferservice/feature/settings/presentation/settings_page.dart'; + +class CustomAppBar extends StatelessWidget { + const CustomAppBar({super.key}); + + void _openSettings(BuildContext context) { + Navigator.of( + context, + ).push(MaterialPageRoute(builder: (context) => SettingsPage())); + } + + @override + Widget build(BuildContext context) { + return AppBar( + title: Center(child: Text("Holzleitner Lieferservice")), + actions: [ + IconButton( + onPressed: () => _openSettings(context), + icon: Icon(Icons.settings), + ), + ], + ); + } +} diff --git a/lib/widget/home/bloc/navigation_bloc.dart b/lib/widget/home/bloc/navigation_bloc.dart index 9768ef5..44aa259 100644 --- a/lib/widget/home/bloc/navigation_bloc.dart +++ b/lib/widget/home/bloc/navigation_bloc.dart @@ -6,23 +6,11 @@ import 'navigation_state.dart'; // Navigation BLoC class NavigationBloc extends Bloc { - NavigationBloc() : super(NavigateToRoute('/scan', index: 0)) { - on((event, emit) { - emit(NavigateToRoute('/cars', index: 2)); - }); - - on((event, emit) { - emit(NavigateToRoute('/deliveries', index: 1)); - }); - - on((event, emit) { - emit(NavigateToRoute('/delivery')); - }); - - on((event, emit) { - emit(NavigateToRoute('/scan', index: 0)); - }); - - // Add more navigation handlers... + NavigationBloc() : super(NavigationInfo(navigationIndex: 0)) { + on(_navigate); } -} \ No newline at end of file + + void _navigate(NavigateToIndex event, Emitter emit) { + emit(NavigationInfo(navigationIndex: event.index)); + } +} diff --git a/lib/widget/home/bloc/navigation_event.dart b/lib/widget/home/bloc/navigation_event.dart index 62e2955..f89988f 100644 --- a/lib/widget/home/bloc/navigation_event.dart +++ b/lib/widget/home/bloc/navigation_event.dart @@ -1,8 +1,7 @@ abstract class NavigationEvent {} -class NavigateToHome extends NavigationEvent {} -class NavigateToDeliveries extends NavigationEvent {} -class NavigateToDelivery extends NavigationEvent {} -class NavigateToScan extends NavigationEvent {} -class NavigateToCars extends NavigationEvent {} -class GoBack extends NavigationEvent {} \ No newline at end of file +class NavigateToIndex extends NavigationEvent { + int index; + + NavigateToIndex({required this.index}); +} \ No newline at end of file diff --git a/lib/widget/home/bloc/navigation_state.dart b/lib/widget/home/bloc/navigation_state.dart index f886386..4d0edd3 100644 --- a/lib/widget/home/bloc/navigation_state.dart +++ b/lib/widget/home/bloc/navigation_state.dart @@ -1,11 +1,8 @@ // Navigation states abstract class NavigationState {} -class NavigationInitial extends NavigationState {} -class NavigateToRoute extends NavigationState { - final String routeName; - final int? index; - final Object? arguments; +class NavigationInfo extends NavigationState { + int navigationIndex; - NavigateToRoute(this.routeName, {this.arguments, this.index}); -} \ No newline at end of file + NavigationInfo({required this.navigationIndex}); +} diff --git a/lib/widget/home/presentation/home.dart b/lib/widget/home/presentation/home.dart index 286c636..d8925aa 100644 --- a/lib/widget/home/presentation/home.dart +++ b/lib/widget/home/presentation/home.dart @@ -6,6 +6,10 @@ import 'package:hl_lieferservice/feature/cars/presentation/car_management_page.d import 'package:hl_lieferservice/feature/delivery/overview/bloc/tour_bloc.dart'; import 'package:hl_lieferservice/feature/delivery/overview/bloc/tour_event.dart'; import 'package:hl_lieferservice/feature/delivery/overview/presentation/delivery_overview_page.dart'; +import 'package:hl_lieferservice/feature/scan/presentation/scan_page.dart'; +import 'package:hl_lieferservice/widget/app_bar.dart'; +import 'package:hl_lieferservice/widget/home/bloc/navigation_bloc.dart'; +import 'package:hl_lieferservice/widget/home/bloc/navigation_state.dart'; import 'package:hl_lieferservice/widget/navigation_bar/presentation/navigation_bar.dart'; import '../../../bloc/app_bloc.dart'; @@ -23,20 +27,18 @@ class Home extends StatefulWidget { } class _HomeState extends State { - int _selectedPage = 0; - @override void initState() { super.initState(); // Load deliveries Authenticated state = context.read().state as Authenticated; - context.read().add(LoadTour(teamId: state.teamId)); + context.read().add(LoadTour(teamId: state.user.number)); } Widget _buildPage(index) { if (index == 0) { - return Container(); + return ScanPage(); } if (index == 1) { @@ -60,20 +62,21 @@ class _HomeState extends State { return Container(); } - void _onSelect(int index) { - setState(() { - _selectedPage = index; - }); - } - @override Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Center(child: Text("Holzleitner Lieferservice")), - ), - body: _buildPage(_selectedPage), - bottomNavigationBar: AppNavigationBar(onSelect: _onSelect), + return BlocBuilder( + builder: (context, state) { + final currentState = state as NavigationInfo; + + return Scaffold( + appBar: PreferredSize( + preferredSize: Size.fromHeight(kToolbarHeight), + child: CustomAppBar(), + ), + body: _buildPage(currentState.navigationIndex), + bottomNavigationBar: AppNavigationBar(), + ); + }, ); } } diff --git a/lib/widget/navigation_bar/presentation/navigation_bar.dart b/lib/widget/navigation_bar/presentation/navigation_bar.dart index e3eb499..7f313cd 100644 --- a/lib/widget/navigation_bar/presentation/navigation_bar.dart +++ b/lib/widget/navigation_bar/presentation/navigation_bar.dart @@ -1,27 +1,24 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:hl_lieferservice/widget/home/bloc/navigation_bloc.dart'; +import 'package:hl_lieferservice/widget/home/bloc/navigation_event.dart'; import 'package:hl_lieferservice/widget/home/bloc/navigation_state.dart'; class AppNavigationBar extends StatefulWidget { - final Function(int) onSelect; - - const AppNavigationBar({required this.onSelect}); + const AppNavigationBar({super.key}); @override State createState() => _AppNavigationBarState(); } class _AppNavigationBarState extends State { - int _selectedPage = 0; - @override Widget build(BuildContext context) { return BlocBuilder( builder: (context, state) { - if (state is NavigateToRoute) { + if (state is NavigationInfo) { return NavigationBar( - selectedIndex: _selectedPage, + selectedIndex: state.navigationIndex, destinations: const [ NavigationDestination( icon: Icon(Icons.barcode_reader), @@ -37,11 +34,7 @@ class _AppNavigationBarState extends State { ), ], onDestinationSelected: (int index) { - widget.onSelect(index); - - setState(() { - _selectedPage = index; - }); + context.read().add(NavigateToIndex(index: index)); }, ); } diff --git a/pubspec.lock b/pubspec.lock index d4f2551..0391449 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -17,6 +17,38 @@ packages: url: "https://pub.dev" source: hosted version: "7.7.1" + app_links: + dependency: "direct main" + description: + name: app_links + sha256: "5f88447519add627fe1cbcab4fd1da3d4fed15b9baf29f28b22535c95ecee3e8" + url: "https://pub.dev" + source: hosted + version: "6.4.1" + app_links_linux: + dependency: transitive + description: + name: app_links_linux + sha256: f5f7173a78609f3dfd4c2ff2c95bd559ab43c80a87dc6a095921d96c05688c81 + url: "https://pub.dev" + source: hosted + version: "1.0.3" + app_links_platform_interface: + dependency: transitive + description: + name: app_links_platform_interface + sha256: "05f5379577c513b534a29ddea68176a4d4802c46180ee8e2e966257158772a3f" + url: "https://pub.dev" + source: hosted + version: "2.0.2" + app_links_web: + dependency: transitive + description: + name: app_links_web + sha256: af060ed76183f9e2b87510a9480e56a5352b6c249778d07bd2c95fc35632a555 + url: "https://pub.dev" + source: hosted + version: "1.0.4" archive: dependency: transitive description: @@ -209,6 +241,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.1" + dbus: + dependency: transitive + description: + name: dbus + sha256: "79e0c23480ff85dc68de79e2cd6334add97e48f7f4865d17686dd6ea81a47e8c" + url: "https://pub.dev" + source: hosted + version: "0.7.11" docuframe: dependency: "direct main" description: @@ -228,10 +268,10 @@ packages: dependency: transitive description: name: fake_async - sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc" + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" url: "https://pub.dev" source: hosted - version: "1.3.2" + version: "1.3.3" ffi: dependency: transitive description: @@ -293,6 +333,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_barcode_listener: + dependency: "direct main" + description: + name: flutter_barcode_listener + sha256: "095b04a29ddab58c623f3a78d88c641b870a6d4bc4bdf0c7a0d4bc58e337aed8" + url: "https://pub.dev" + source: hosted + version: "0.1.4" flutter_bloc: dependency: "direct main" description: @@ -343,6 +391,78 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.0" + geoclue: + dependency: transitive + description: + name: geoclue + sha256: c2a998c77474fc57aa00c6baa2928e58f4b267649057a1c76738656e9dbd2a7f + url: "https://pub.dev" + source: hosted + version: "0.1.1" + geolocator: + dependency: "direct main" + description: + name: geolocator + sha256: "79939537046c9025be47ec645f35c8090ecadb6fe98eba146a0d25e8c1357516" + url: "https://pub.dev" + source: hosted + version: "14.0.2" + geolocator_android: + dependency: transitive + description: + name: geolocator_android + sha256: "179c3cb66dfa674fc9ccbf2be872a02658724d1c067634e2c427cf6df7df901a" + url: "https://pub.dev" + source: hosted + version: "5.0.2" + geolocator_apple: + dependency: transitive + description: + name: geolocator_apple + sha256: dbdd8789d5aaf14cf69f74d4925ad1336b4433a6efdf2fce91e8955dc921bf22 + url: "https://pub.dev" + source: hosted + version: "2.3.13" + geolocator_linux: + dependency: transitive + description: + name: geolocator_linux + sha256: c4e966f0a7a87e70049eac7a2617f9e16fd4c585a26e4330bdfc3a71e6a721f3 + url: "https://pub.dev" + source: hosted + version: "0.2.3" + geolocator_platform_interface: + dependency: transitive + description: + name: geolocator_platform_interface + sha256: "30cb64f0b9adcc0fb36f628b4ebf4f731a2961a0ebd849f4b56200205056fe67" + url: "https://pub.dev" + source: hosted + version: "4.2.6" + geolocator_web: + dependency: transitive + description: + name: geolocator_web + sha256: b1ae9bdfd90f861fde8fd4f209c37b953d65e92823cb73c7dee1fa021b06f172 + url: "https://pub.dev" + source: hosted + version: "4.1.3" + geolocator_windows: + dependency: transitive + description: + name: geolocator_windows + sha256: "175435404d20278ffd220de83c2ca293b73db95eafbdc8131fe8609be1421eb6" + url: "https://pub.dev" + source: hosted + version: "0.2.5" + get_it: + dependency: "direct main" + description: + name: get_it + sha256: a4292e7cf67193f8e7c1258203104eb2a51ec8b3a04baa14695f4064c144297b + url: "https://pub.dev" + source: hosted + version: "8.2.0" glob: dependency: transitive description: @@ -359,6 +479,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.2" + gsettings: + dependency: transitive + description: + name: gsettings + sha256: "1b0ce661f5436d2db1e51f3c4295a49849f03d304003a7ba177d01e3a858249c" + url: "https://pub.dev" + source: hosted + version: "0.2.8" + gtk: + dependency: transitive + description: + name: gtk + sha256: e8ce9ca4b1df106e4d72dad201d345ea1a036cc12c360f1a7d5a758f78ffa42c + url: "https://pub.dev" + source: hosted + version: "2.1.0" http: dependency: "direct main" description: @@ -376,7 +512,7 @@ packages: source: hosted version: "3.2.2" http_parser: - dependency: transitive + dependency: "direct main" description: name: http_parser sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" @@ -491,26 +627,26 @@ packages: dependency: transitive description: name: leak_tracker - sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" url: "https://pub.dev" source: hosted - version: "10.0.8" + version: "11.0.2" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" url: "https://pub.dev" source: hosted - version: "3.0.9" + version: "3.0.10" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" lints: dependency: transitive description: @@ -567,6 +703,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.0" + mobile_scanner: + dependency: "direct main" + description: + name: mobile_scanner + sha256: "023a71afb4d7cfb5529d0f2636aa8b43db66257905b9486d702085989769c5f2" + url: "https://pub.dev" + source: hosted + version: "7.1.3" mocktail: dependency: transitive description: @@ -591,6 +735,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.0" + package_info_plus: + dependency: transitive + description: + name: package_info_plus + sha256: "16eee997588c60225bda0488b6dcfac69280a6b7a3cf02c741895dd370a02968" + url: "https://pub.dev" + source: hosted + version: "8.3.1" + package_info_plus_platform_interface: + dependency: transitive + description: + name: package_info_plus_platform_interface + sha256: "202a487f08836a592a6bd4f901ac69b3a8f146af552bbd14407b6b41e1c3f086" + url: "https://pub.dev" + source: hosted + version: "3.2.1" path: dependency: transitive description: @@ -719,6 +879,62 @@ packages: url: "https://pub.dev" source: hosted version: "1.5.0" + shared_preferences: + dependency: "direct main" + description: + name: shared_preferences + sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5" + url: "https://pub.dev" + source: hosted + version: "2.5.3" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: "34266009473bf71d748912da4bf62d439185226c03e01e2d9687bc65bbfcb713" + url: "https://pub.dev" + source: hosted + version: "2.4.15" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "1c33a907142607c40a7542768ec9badfd16293bac51da3a4482623d15845f88b" + url: "https://pub.dev" + source: hosted + version: "2.5.5" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 + url: "https://pub.dev" + source: hosted + version: "2.4.3" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" + url: "https://pub.dev" + source: hosted + version: "2.4.1" shelf: dependency: transitive description: @@ -772,6 +988,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.10.1" + sprintf: + dependency: transitive + description: + name: sprintf + sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" + url: "https://pub.dev" + source: hosted + version: "7.0.0" stack_trace: dependency: transitive description: @@ -816,10 +1040,10 @@ packages: dependency: transitive description: name: test_api - sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd + sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" url: "https://pub.dev" source: hosted - version: "0.7.4" + version: "0.7.6" timing: dependency: transitive description: @@ -836,6 +1060,78 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.0" + url_launcher: + dependency: "direct main" + description: + name: url_launcher + sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8 + url: "https://pub.dev" + source: hosted + version: "6.3.2" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + sha256: "81777b08c498a292d93ff2feead633174c386291e35612f8da438d6e92c4447e" + url: "https://pub.dev" + source: hosted + version: "6.3.20" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + sha256: d80b3f567a617cb923546034cc94bfe44eb15f989fe670b37f26abdb9d939cb7 + url: "https://pub.dev" + source: hosted + version: "6.3.4" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935" + url: "https://pub.dev" + source: hosted + version: "3.2.1" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + sha256: c043a77d6600ac9c38300567f33ef12b0ef4f4783a2c1f00231d2b1941fea13f + url: "https://pub.dev" + source: hosted + version: "3.2.3" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + sha256: "4bd2b7b4dc4d4d0b94e5babfffbca8eac1a126c7f3d6ecbc1a11013faa3abba2" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77" + url: "https://pub.dev" + source: hosted + version: "3.1.4" + uuid: + dependency: transitive + description: + name: uuid + sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff + url: "https://pub.dev" + source: hosted + version: "4.5.1" vector_graphics: dependency: transitive description: @@ -864,10 +1160,10 @@ packages: dependency: transitive description: name: vector_math - sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.2.0" vm_service: dependency: transitive description: @@ -908,6 +1204,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.3" + win32: + dependency: transitive + description: + name: win32 + sha256: "329edf97fdd893e0f1e3b9e88d6a0e627128cc17cc316a8d67fda8f1451178ba" + url: "https://pub.dev" + source: hosted + version: "5.13.0" xdg_directories: dependency: transitive description: @@ -933,5 +1237,5 @@ packages: source: hosted version: "3.1.3" sdks: - dart: ">=3.7.2 <4.0.0" - flutter: ">=3.29.0" + dart: ">=3.9.0 <4.0.0" + flutter: ">=3.35.0" diff --git a/pubspec.yaml b/pubspec.yaml index 6f1428c..5480eb1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -45,6 +45,14 @@ dependencies: easy_stepper: ^0.8.5+1 carousel_slider: ^5.1.1 signature: ^6.3.0 + url_launcher: ^6.3.2 + app_links: ^6.4.1 + get_it: ^8.2.0 + http_parser: ^4.1.2 + flutter_barcode_listener: ^0.1.4 + geolocator: ^14.0.2 + mobile_scanner: ^7.1.3 + shared_preferences: ^2.5.3 dev_dependencies: build_runner: ^2.5.4