diff --git a/.idea/App-Gaslieferung.iml b/.idea/App-Gaslieferung.iml index ae9af97..d327551 100644 --- a/.idea/App-Gaslieferung.iml +++ b/.idea/App-Gaslieferung.iml @@ -11,5 +11,6 @@ + \ No newline at end of file diff --git a/.idea/libraries/Dart_Packages.xml b/.idea/libraries/Dart_Packages.xml index bccc341..bf68cca 100644 --- a/.idea/libraries/Dart_Packages.xml +++ b/.idea/libraries/Dart_Packages.xml @@ -2,6 +2,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.idea/libraries/Flutter_Plugins.xml b/.idea/libraries/Flutter_Plugins.xml index b0f6971..a7eb7b4 100644 --- a/.idea/libraries/Flutter_Plugins.xml +++ b/.idea/libraries/Flutter_Plugins.xml @@ -1,6 +1,18 @@ - + + + + + + + + + + + + + diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 08d0737..e8fc3ff 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -24,6 +24,20 @@ + + + + + + + + + + + + + + diff --git a/assets/graphics/gas-tank.png b/assets/graphics/gas-tank.png index 8588aee..05c9ef3 100644 Binary files a/assets/graphics/gas-tank.png and b/assets/graphics/gas-tank.png differ diff --git a/assets/graphics/location-pin-cloud.png b/assets/graphics/location-pin-cloud.png index 81dbd02..99d5630 100644 Binary files a/assets/graphics/location-pin-cloud.png and b/assets/graphics/location-pin-cloud.png differ diff --git a/assets/graphics/truck.png b/assets/graphics/truck.png new file mode 100644 index 0000000..7d72bba Binary files /dev/null and b/assets/graphics/truck.png differ diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index e03825a..179125b 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -45,5 +45,19 @@ UIApplicationSupportsIndirectInputEvents + + CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLName + myapp + CFBundleURLSchemes + + myapp + + + diff --git a/lib/bloc/authentication/auth_bloc.dart b/lib/bloc/authentication/auth_bloc.dart new file mode 100644 index 0000000..1420dc2 --- /dev/null +++ b/lib/bloc/authentication/auth_bloc.dart @@ -0,0 +1,82 @@ +import 'package:app_gaslieferung/bloc/authentication/auth_event.dart'; +import 'package:app_gaslieferung/bloc/authentication/auth_state.dart'; +import 'package:app_gaslieferung/bloc/message_wrapper/message_bloc.dart'; +import 'package:app_gaslieferung/bloc/message_wrapper/message_event.dart'; +import 'package:app_links/app_links.dart'; +import 'package:bloc/bloc.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:url_launcher/url_launcher.dart'; + +import '../../exceptions/login.dart'; +import '../../util/login.dart'; + +class AuthBloc extends Bloc { + final _appLinks = AppLinks(); + final MessageBloc msgBloc; + final String url; + + AuthBloc({required this.url, required this.msgBloc}) : super(UnauthenticatedState()) { + on(_onLoginEvent); + on(_onLogoutEvent); + on(_onFailedEvent); + on(_onLoginSuccessEvent); + + _initDeepLinks(); + } + + void _initDeepLinks() async { + // Listen to the stream if any further deeplink is coming in. + _appLinks.uriLinkStream.listen((uri) { + _handleDeepLink(uri); + }); + } + + /// Handle Deeplink if login request is coming in. + void _handleDeepLink(Uri uri) { + if (state is UnauthenticatedState) { + try { + add(AuthLoginSuccessEvent(getSessionIdFromUrl(uri))); + } on LoginInvalidUrlException catch (e) { + msgBloc.add(MessageShow(message: e.toString())); + } on LoginNoSessionIdException catch (e) { + msgBloc.add(MessageShow(message: e.toString())); + } catch (e, st) { + debugPrint("Fehler beim Login. Stacktrace: $st"); + debugPrint("Fehler beim Login. Message: $e"); + + add( + AuthFailedEvent( + message: + "Es ist ein unbekannter Fehler aufgetreten. Versuchen Sie es später erneut oder wenden Sie sich an die Zentrale.", + ), + ); + } + } else { + // TODO: handle message if user is already logged in + debugPrint("NOT STATE"); + debugPrint("$state"); + } + } + + void _onLoginSuccessEvent( + AuthLoginSuccessEvent event, + Emitter emit, + ) { + emit(AuthenticatedState(sessionId: event.sessionId)); + } + + void _onLoginEvent(AuthLoginEvent event, Emitter emit) async { + await launchUrl( + Uri.parse(url), + mode: LaunchMode.externalApplication, + ); + } + + void _onLogoutEvent(AuthLogoutEvent event, Emitter emit) { + + } + + void _onFailedEvent(AuthFailedEvent event, Emitter emit) { + emit(UnauthenticatedState()); + } +} diff --git a/lib/bloc/authentication/auth_event.dart b/lib/bloc/authentication/auth_event.dart new file mode 100644 index 0000000..2463a39 --- /dev/null +++ b/lib/bloc/authentication/auth_event.dart @@ -0,0 +1,17 @@ +abstract class AuthEvent {} + +class AuthLoginEvent extends AuthEvent {} + +class AuthLoginSuccessEvent extends AuthEvent { + String sessionId; + + AuthLoginSuccessEvent(this.sessionId); +} + +class AuthLogoutEvent extends AuthEvent {} + +class AuthFailedEvent extends AuthEvent { + String message; + + AuthFailedEvent({required this.message}); +} diff --git a/lib/bloc/authentication/auth_state.dart b/lib/bloc/authentication/auth_state.dart new file mode 100644 index 0000000..0c628e7 --- /dev/null +++ b/lib/bloc/authentication/auth_state.dart @@ -0,0 +1,17 @@ +abstract class AuthState {} + +class AuthInitialState extends AuthState {} + +class UnauthenticatedState extends AuthState {} + +class AuthenticatedState extends AuthState { + String sessionId; + + AuthenticatedState({required this.sessionId}); +} + +class AuthFailedState extends AuthState { + String message; + + AuthFailedState({required this.message}); +} diff --git a/lib/bloc/message_wrapper/message_bloc.dart b/lib/bloc/message_wrapper/message_bloc.dart new file mode 100644 index 0000000..9eb2b8b --- /dev/null +++ b/lib/bloc/message_wrapper/message_bloc.dart @@ -0,0 +1,21 @@ +import 'package:bloc/bloc.dart'; + +import 'message_event.dart'; +import 'message_state.dart'; + +class MessageBloc extends Bloc { + MessageBloc() : super(MessageInitialState()) { + on(_show); + on(_hide); + } + + void _show(MessageShow event, Emitter emit) async { + emit(MessageShowState(message: event.message)); + await Future.delayed(event.duration); + emit(MessageHideState()); + } + + void _hide(MessageHide event, Emitter emit) { + emit(MessageHideState()); + } +} \ No newline at end of file diff --git a/lib/bloc/message_wrapper/message_event.dart b/lib/bloc/message_wrapper/message_event.dart new file mode 100644 index 0000000..976e8fb --- /dev/null +++ b/lib/bloc/message_wrapper/message_event.dart @@ -0,0 +1,10 @@ +abstract class MessageEvent {} + +class MessageShow extends MessageEvent { + String message; + Duration duration; + + MessageShow({required this.message}) : duration = Duration(seconds: 5); +} + +class MessageHide extends MessageEvent {} diff --git a/lib/bloc/message_wrapper/message_state.dart b/lib/bloc/message_wrapper/message_state.dart new file mode 100644 index 0000000..44b26b5 --- /dev/null +++ b/lib/bloc/message_wrapper/message_state.dart @@ -0,0 +1,7 @@ +abstract class MessageState {} +class MessageInitialState extends MessageState {} +class MessageShowState extends MessageState { + String message; + MessageShowState({required this.message}); +} +class MessageHideState extends MessageState {} \ No newline at end of file diff --git a/lib/bloc/tour/tour_bloc.dart b/lib/bloc/tour/tour_bloc.dart new file mode 100644 index 0000000..9f7ad38 --- /dev/null +++ b/lib/bloc/tour/tour_bloc.dart @@ -0,0 +1,25 @@ +import 'package:app_gaslieferung/repository/tour_repository.dart'; +import 'package:bloc/bloc.dart'; +import 'package:flutter/cupertino.dart'; + +import 'tour_event.dart'; +import 'tour_state.dart'; + +class TourBloc extends Bloc { + TourRepository tour; + + TourBloc({required this.tour}) : super(TourInitial()) { + on(_onTourLoadEvent); + } + + void _onTourLoadEvent(TourLoadEvent event, Emitter emit) async { + emit(TourLoading()); + try { + final tour = await this.tour.getTour(event.sessionId, event.carId); + emit(TourLoaded(tour: tour)); + } catch (e, st) { + debugPrint(st.toString()); + emit(TourLoadingFailed(message: e.toString())); + } + } +} diff --git a/lib/bloc/tour/tour_event.dart b/lib/bloc/tour/tour_event.dart new file mode 100644 index 0000000..5ce8494 --- /dev/null +++ b/lib/bloc/tour/tour_event.dart @@ -0,0 +1,8 @@ +abstract class TourEvent {} + +class TourLoadEvent extends TourEvent { + String carId; + String sessionId; + + TourLoadEvent({required this.carId, required this.sessionId}); +} \ No newline at end of file diff --git a/lib/bloc/tour/tour_state.dart b/lib/bloc/tour/tour_state.dart new file mode 100644 index 0000000..29e631b --- /dev/null +++ b/lib/bloc/tour/tour_state.dart @@ -0,0 +1,20 @@ +import 'package:app_gaslieferung/model/delivery.dart'; +import 'package:app_gaslieferung/model/tour.dart'; + +abstract class TourState {} + +class TourInitial extends TourState {} + +class TourLoaded extends TourState { + Tour tour; + + TourLoaded({required this.tour}); +} + +class TourLoading extends TourState {} + +class TourLoadingFailed extends TourState { + String message; + + TourLoadingFailed({required this.message}); +} \ No newline at end of file diff --git a/lib/bloc/tour_select/bloc.dart b/lib/bloc/tour_select/bloc.dart index 444b20a..ca5833f 100644 --- a/lib/bloc/tour_select/bloc.dart +++ b/lib/bloc/tour_select/bloc.dart @@ -1,14 +1,27 @@ -import 'package:app_gaslieferung/repository/tour_select_repository.dart'; +import 'package:app_gaslieferung/model/car.dart'; +import 'package:app_gaslieferung/repository/tour_repository.dart'; import 'package:bloc/bloc.dart'; +import 'package:flutter/material.dart'; import 'event.dart'; import 'state.dart'; class TourSelectBloc extends Bloc { - TourSelectRepository repository; + TourRepository repository; TourSelectBloc({required this.repository}) : super(TourSelectInitial()) { - on((event, emit) { - // TODO: handle event - }); + on(_onTourSelectLoadCarsEvent); } -} \ No newline at end of file + + void _onTourSelectLoadCarsEvent( + TourSelectLoadMetadataEvent event, + Emitter emit, + ) async { + emit(TourSelectLoading()); + try { + emit(TourSelectLoaded(data: await repository.getSupplierTourMetadata(event.sessionId))); + } catch (e,st) { + debugPrint("Error on loading cars: $e\n$st"); + emit(TourSelectError(message: "Es ist ein unbekannter Fehler aufgetreten. Versuchen Sie es erneut.")); + } + } +} diff --git a/lib/bloc/tour_select/event.dart b/lib/bloc/tour_select/event.dart index f8fff32..8055b35 100644 --- a/lib/bloc/tour_select/event.dart +++ b/lib/bloc/tour_select/event.dart @@ -1,5 +1,7 @@ abstract class TourSelectEvent {} -class TourSelectLoadingEvent extends TourSelectEvent {} -class TourSelectLoadedEvent extends TourSelectEvent {} -class TourSelectErrorEvent extends TourSelectEvent {} +class TourSelectLoadMetadataEvent extends TourSelectEvent { + String sessionId; + + TourSelectLoadMetadataEvent({required this.sessionId}); +} \ No newline at end of file diff --git a/lib/bloc/tour_select/state.dart b/lib/bloc/tour_select/state.dart index d0cc963..1efadf4 100644 --- a/lib/bloc/tour_select/state.dart +++ b/lib/bloc/tour_select/state.dart @@ -1,3 +1,19 @@ +import 'package:app_gaslieferung/model/supplier.dart'; + abstract class TourSelectState {} -class TourSelectInitial extends TourSelectState {} \ No newline at end of file +class TourSelectInitial extends TourSelectState {} + +class TourSelectLoading extends TourSelectState {} + +class TourSelectLoaded extends TourSelectState { + SupplierTourMetadata data; + + TourSelectLoaded({required this.data}); +} + +class TourSelectError extends TourSelectState { + String message; + + TourSelectError({required this.message}); +} diff --git a/lib/dto/car_dto.dart b/lib/dto/car_dto.dart new file mode 100644 index 0000000..477199c --- /dev/null +++ b/lib/dto/car_dto.dart @@ -0,0 +1,25 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'car_dto.g.dart'; + +@JsonSerializable() +class CarDTO { + @JsonKey(name: 'id') + final int id; + + @JsonKey(name: 'driver_name') + final String? driverName; + + @JsonKey(name: 'car_name') + final String carName; + + CarDTO({ + required this.id, + required this.carName, + required this.driverName, + }); + + factory CarDTO.fromJson(Map json) => _$CarDTOFromJson(json); + + Map toJson() => _$CarDTOToJson(this); +} \ No newline at end of file diff --git a/lib/dto/car_dto.g.dart b/lib/dto/car_dto.g.dart new file mode 100644 index 0000000..0061de8 --- /dev/null +++ b/lib/dto/car_dto.g.dart @@ -0,0 +1,19 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'car_dto.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +CarDTO _$CarDTOFromJson(Map json) => CarDTO( + id: (json['id'] as num).toInt(), + carName: json['car_name'] as String, + driverName: json['driver_name'] as String?, +); + +Map _$CarDTOToJson(CarDTO instance) => { + 'id': instance.id, + 'driver_name': instance.driverName, + 'car_name': instance.carName, +}; diff --git a/lib/dto/delivery_dto.dart b/lib/dto/delivery_dto.dart new file mode 100644 index 0000000..3df01b5 --- /dev/null +++ b/lib/dto/delivery_dto.dart @@ -0,0 +1,20 @@ +import 'package:json_annotation/json_annotation.dart'; +import 'car_dto.dart'; + +part 'delivery_dto.g.dart'; + +@JsonSerializable(explicitToJson: true) +class DeliveryDTO { + @JsonKey(name: 'amount_deliveries') + final int amountDeliveries; + + final CarDTO car; + + DeliveryDTO({ + required this.amountDeliveries, + required this.car, + }); + + factory DeliveryDTO.fromJson(Map json) => _$DeliveryDTOFromJson(json); + Map toJson() => _$DeliveryDTOToJson(this); +} \ No newline at end of file diff --git a/lib/dto/delivery_dto.g.dart b/lib/dto/delivery_dto.g.dart new file mode 100644 index 0000000..f8e1798 --- /dev/null +++ b/lib/dto/delivery_dto.g.dart @@ -0,0 +1,18 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'delivery_dto.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +DeliveryDTO _$DeliveryDTOFromJson(Map json) => DeliveryDTO( + amountDeliveries: (json['amount_deliveries'] as num).toInt(), + car: CarDTO.fromJson(json['car'] as Map), +); + +Map _$DeliveryDTOToJson(DeliveryDTO instance) => + { + 'amount_deliveries': instance.amountDeliveries, + 'car': instance.car.toJson(), + }; diff --git a/lib/dto/fail_response_dto.dart b/lib/dto/fail_response_dto.dart new file mode 100644 index 0000000..cc2dd65 --- /dev/null +++ b/lib/dto/fail_response_dto.dart @@ -0,0 +1,14 @@ +import 'package:json_annotation/json_annotation.dart'; + +part 'fail_response_dto.g.dart'; + +@JsonSerializable() +class FailResponseDTO { + int? code; + String message; + + FailResponseDTO({required this.message, required this.code}); + + factory FailResponseDTO.fromJson(Map json) => _$FailResponseDTOFromJson(json); + Map toJson() => _$FailResponseDTOToJson(this); +} diff --git a/lib/dto/fail_response_dto.g.dart b/lib/dto/fail_response_dto.g.dart new file mode 100644 index 0000000..39d5afc --- /dev/null +++ b/lib/dto/fail_response_dto.g.dart @@ -0,0 +1,16 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'fail_response_dto.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +FailResponseDTO _$FailResponseDTOFromJson(Map json) => + FailResponseDTO( + message: json['message'] as String, + code: (json['code'] as num?)?.toInt(), + ); + +Map _$FailResponseDTOToJson(FailResponseDTO instance) => + {'code': instance.code, 'message': instance.message}; diff --git a/lib/dto/get_deliveries_dto.dart b/lib/dto/get_deliveries_dto.dart new file mode 100644 index 0000000..ae2ce42 --- /dev/null +++ b/lib/dto/get_deliveries_dto.dart @@ -0,0 +1,19 @@ +import 'package:json_annotation/json_annotation.dart'; +import 'delivery_dto.dart'; + +part 'get_deliveries_dto.g.dart'; + +@JsonSerializable(explicitToJson: true) +class GetDeliveriesDTO { + final List deliveries; + final String date; + + GetDeliveriesDTO({ + required this.deliveries, + required this.date + }); + + factory GetDeliveriesDTO.fromJson(Map json) => _$GetDeliveriesDTOFromJson(json); + + Map toJson() => _$GetDeliveriesDTOToJson(this); +} \ No newline at end of file diff --git a/lib/dto/get_deliveries_dto.g.dart b/lib/dto/get_deliveries_dto.g.dart new file mode 100644 index 0000000..e375f0b --- /dev/null +++ b/lib/dto/get_deliveries_dto.g.dart @@ -0,0 +1,21 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'get_deliveries_dto.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +GetDeliveriesDTO _$GetDeliveriesDTOFromJson(Map json) => + GetDeliveriesDTO( + deliveries: (json['deliveries'] as List) + .map((e) => DeliveryDTO.fromJson(e as Map)) + .toList(), + date: json['date'] as String, + ); + +Map _$GetDeliveriesDTOToJson(GetDeliveriesDTO instance) => + { + 'deliveries': instance.deliveries.map((e) => e.toJson()).toList(), + 'date': instance.date, + }; diff --git a/lib/exceptions/login.dart b/lib/exceptions/login.dart new file mode 100644 index 0000000..502f768 --- /dev/null +++ b/lib/exceptions/login.dart @@ -0,0 +1,20 @@ +class LoginInvalidUrlException implements Exception { + @override + String toString() { + return "Invalid url"; + } +} + +class LoginNoSessionIdException implements Exception { + @override + String toString() { + return "No session id found in url"; + } +} + +class LoginUnauthorizedException implements Exception { + @override + String toString() { + return "Unauthorized"; + } +} \ No newline at end of file diff --git a/lib/exceptions/server.dart b/lib/exceptions/server.dart new file mode 100644 index 0000000..01963ca --- /dev/null +++ b/lib/exceptions/server.dart @@ -0,0 +1,8 @@ +class ServerErrorException implements Exception { + final String message; + + ServerErrorException({required this.message}); + + @override + String toString() => message; +} diff --git a/lib/main.dart b/lib/main.dart index 2a4aba3..2fd8f22 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,23 +1,55 @@ +import 'package:app_gaslieferung/bloc/authentication/auth_bloc.dart'; +import 'package:app_gaslieferung/bloc/tour/tour_bloc.dart'; +import 'package:app_gaslieferung/bloc/tour_select/bloc.dart'; +import 'package:app_gaslieferung/repository/tour_repository.dart'; +import 'package:app_gaslieferung/service/tour_service.dart'; import 'package:app_gaslieferung/ui/page/login.dart'; import 'package:app_gaslieferung/ui/theme/theme.dart'; +import 'package:app_gaslieferung/ui/widgets/message_wrapper/message_wrapper.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'bloc/message_wrapper/message_bloc.dart'; void main() { - runApp(const MyApp()); + runApp(GasDeliveryApp()); } -class MyApp extends StatelessWidget { - const MyApp({super.key}); +class GasDeliveryApp extends StatefulWidget { + const GasDeliveryApp({super.key}); - // This widget is the root of your application. + @override + State createState() => _GasDeliveryAppState(); +} + +class _GasDeliveryAppState extends State { @override Widget build(BuildContext context) { - return MaterialApp( - title: 'Flutter Demo', - theme: AppTheme.light(), - darkTheme: AppTheme.dark(), - themeMode: ThemeMode.light, // oder ThemeMode.dark / light - home: const LoginPage(), + return MultiBlocProvider( + providers: [ + BlocProvider(create: (context) => MessageBloc()), + BlocProvider( + create: (context) => AuthBloc( + url: "http://127.0.0.1:3000/login", + msgBloc: context.read(), + ), + ), + BlocProvider( + create: (context) => TourBloc( + tour: TourRepository( + service: TourService(baseUrl: "http://127.0.0.1:3000"), + ), + ), + ), + ], + child: MaterialApp( + builder: (context, child) => MessageWrapperWidget(child: child!), + theme: AppTheme.light(), + darkTheme: AppTheme.dark(), + themeMode: ThemeMode.light, + // oder ThemeMode.dark / light + home: LoginPage(), + ), ); } -} \ No newline at end of file +} diff --git a/lib/model/car.dart b/lib/model/car.dart index 4e411d1..204830f 100644 --- a/lib/model/car.dart +++ b/lib/model/car.dart @@ -1,7 +1,19 @@ -class Car { - String id; - String carName; - String? driverName; +import 'package:json_annotation/json_annotation.dart'; - Car({required this.id, required this.carName, this.driverName}); -} \ No newline at end of file +part 'car.g.dart'; + +@JsonSerializable(fieldRename: FieldRename.snake) +class Car { + final int id; + final String carName; + final String? driverName; + + Car({ + required this.id, + required this.carName, + this.driverName, + }); + + factory Car.fromJson(Map json) => _$CarFromJson(json); + Map toJson() => _$CarToJson(this); +} diff --git a/lib/model/car.g.dart b/lib/model/car.g.dart new file mode 100644 index 0000000..50ac51c --- /dev/null +++ b/lib/model/car.g.dart @@ -0,0 +1,19 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'car.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +Car _$CarFromJson(Map json) => Car( + id: (json['id'] as num).toInt(), + carName: json['car_name'] as String, + driverName: json['driver_name'] as String?, +); + +Map _$CarToJson(Car instance) => { + 'id': instance.id, + 'car_name': instance.carName, + 'driver_name': instance.driverName, +}; diff --git a/lib/model/delivery.dart b/lib/model/delivery.dart index e191524..ea863ac 100644 --- a/lib/model/delivery.dart +++ b/lib/model/delivery.dart @@ -7,13 +7,14 @@ class Delivery { DateTime? deliveredAt; DateTime? desiredDeliveryTime; - String? driverInformation; + + String? informationForDriver; Delivery({ required this.receipt, this.desiredDeliveryTime, - this.driverInformation, + this.informationForDriver, this.deliveredAt }); } \ No newline at end of file diff --git a/lib/model/supplier.dart b/lib/model/supplier.dart index b8a3500..2a6842c 100644 --- a/lib/model/supplier.dart +++ b/lib/model/supplier.dart @@ -16,3 +16,21 @@ class Supplier { required this.address, }); } + +/// Contains information about the cars the supplier has and how many +/// deliveries they have per car. +class SupplierTourMetadata { + String date; + + /// The list of cars the supplier has and how many deliveries they have per car. + List tours; + + SupplierTourMetadata({required this.date, required this.tours}); +} + +class TourMetadata { + Car car; + int amountDeliveries; + + TourMetadata({required this.car, required this.amountDeliveries}); +} \ No newline at end of file diff --git a/lib/model/tour.dart b/lib/model/tour.dart index cb7b6a0..0d1a0e6 100644 --- a/lib/model/tour.dart +++ b/lib/model/tour.dart @@ -1,10 +1,129 @@ -import 'supplier.dart'; -import 'delivery.dart'; +import 'package:json_annotation/json_annotation.dart'; +import 'car.dart'; + +part 'tour.g.dart'; + +@JsonSerializable(fieldRename: FieldRename.snake) +class Article { + final int id; + final String title; + final int quantity; + final double pricePerQuantity; + final double depositPricePerQuantity; + final int quantityDelivered; + final int quantityReturned; + final int quantityToDeposit; + final int? referenceTo; + + Article({ + required this.id, + required this.title, + required this.quantity, + required this.pricePerQuantity, + required this.depositPricePerQuantity, + required this.quantityDelivered, + required this.quantityReturned, + required this.quantityToDeposit, + this.referenceTo, + }); + + factory Article.fromJson(Map json) => _$ArticleFromJson(json); + Map toJson() => _$ArticleToJson(this); +} + +@JsonSerializable(fieldRename: FieldRename.snake) +class Address { + final String street; + final String housingNumber; + final String postalCode; + final String city; + final String? country; + + Address({ + required this.street, + required this.housingNumber, + required this.postalCode, + required this.city, + this.country, + }); + + factory Address.fromJson(Map json) => _$AddressFromJson(json); + Map toJson() => _$AddressToJson(this); +} + +@JsonSerializable(fieldRename: FieldRename.snake) +class Customer { + final String name; + final int id; + final Address address; + + Customer({ + required this.name, + required this.id, + required this.address, + }); + + String get displayAddress => "${address.street} ${address.housingNumber}, ${address.postalCode} ${address.city}"; + + factory Customer.fromJson(Map json) => _$CustomerFromJson(json); + Map toJson() => _$CustomerToJson(this); +} + +@JsonSerializable(fieldRename: FieldRename.snake) +class Receipt { + final List
articles; + final Customer customer; + final double totalGrossPrice; + final double totalNetPrice; + + Receipt({ + required this.articles, + required this.customer, + required this.totalGrossPrice, + required this.totalNetPrice, + }); + + factory Receipt.fromJson(Map json) => _$ReceiptFromJson(json); + Map toJson() => _$ReceiptToJson(this); +} + +@JsonSerializable(fieldRename: FieldRename.snake) +class Delivery { + final Receipt receipt; + final String informationForDriver; + final DateTime desiredDeliveryTime; + final DateTime? timeDelivered; + + Delivery({ + required this.receipt, + required this.informationForDriver, + required this.desiredDeliveryTime, + required this.timeDelivered, + }); + + factory Delivery.fromJson(Map json) => _$DeliveryFromJson(json); + Map toJson() => _$DeliveryToJson(this); +} + +@JsonSerializable(fieldRename: FieldRename.snake) class Tour { - DateTime date; - Supplier supplier; - List deliveries; + final Car car; + final String date; + final List deliveries; - Tour({required this.date, required this.deliveries, required this.supplier}); + Tour({ + required this.car, + required this.date, + required this.deliveries, + }); + + int get amountDeliveries => deliveries.length; + int get amountFinishedDeliveries => deliveries.where((delivery) => delivery.timeDelivered != null).length; + int get amountDeliveriesLeft => amountDeliveries - amountFinishedDeliveries; + double get progress => amountFinishedDeliveries / amountDeliveries; + int get progressPercentage => (progress * 100).toInt(); + + factory Tour.fromJson(Map json) => _$TourFromJson(json); + Map toJson() => _$TourToJson(this); } \ No newline at end of file diff --git a/lib/model/tour.g.dart b/lib/model/tour.g.dart new file mode 100644 index 0000000..158d1b9 --- /dev/null +++ b/lib/model/tour.g.dart @@ -0,0 +1,106 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'tour.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +Article _$ArticleFromJson(Map json) => Article( + id: (json['id'] as num).toInt(), + title: json['title'] as String, + quantity: (json['quantity'] as num).toInt(), + pricePerQuantity: (json['price_per_quantity'] as num).toDouble(), + depositPricePerQuantity: (json['deposit_price_per_quantity'] as num) + .toDouble(), + quantityDelivered: (json['quantity_delivered'] as num).toInt(), + quantityReturned: (json['quantity_returned'] as num).toInt(), + quantityToDeposit: (json['quantity_to_deposit'] as num).toInt(), + referenceTo: (json['reference_to'] as num?)?.toInt(), +); + +Map _$ArticleToJson(Article instance) => { + 'id': instance.id, + 'title': instance.title, + 'quantity': instance.quantity, + 'price_per_quantity': instance.pricePerQuantity, + 'deposit_price_per_quantity': instance.depositPricePerQuantity, + 'quantity_delivered': instance.quantityDelivered, + 'quantity_returned': instance.quantityReturned, + 'quantity_to_deposit': instance.quantityToDeposit, + 'reference_to': instance.referenceTo, +}; + +Address _$AddressFromJson(Map json) => Address( + street: json['street'] as String, + housingNumber: json['housing_number'] as String, + postalCode: json['postal_code'] as String, + city: json['city'] as String, + country: json['country'] as String?, +); + +Map _$AddressToJson(Address instance) => { + 'street': instance.street, + 'housing_number': instance.housingNumber, + 'postal_code': instance.postalCode, + 'city': instance.city, + 'country': instance.country, +}; + +Customer _$CustomerFromJson(Map json) => Customer( + name: json['name'] as String, + id: (json['id'] as num).toInt(), + address: Address.fromJson(json['address'] as Map), +); + +Map _$CustomerToJson(Customer instance) => { + 'name': instance.name, + 'id': instance.id, + 'address': instance.address, +}; + +Receipt _$ReceiptFromJson(Map json) => Receipt( + articles: (json['articles'] as List) + .map((e) => Article.fromJson(e as Map)) + .toList(), + customer: Customer.fromJson(json['customer'] as Map), + totalGrossPrice: (json['total_gross_price'] as num).toDouble(), + totalNetPrice: (json['total_net_price'] as num).toDouble(), +); + +Map _$ReceiptToJson(Receipt instance) => { + 'articles': instance.articles, + 'customer': instance.customer, + 'total_gross_price': instance.totalGrossPrice, + 'total_net_price': instance.totalNetPrice, +}; + +Delivery _$DeliveryFromJson(Map json) => Delivery( + receipt: Receipt.fromJson(json['receipt'] as Map), + informationForDriver: json['information_for_driver'] as String, + desiredDeliveryTime: DateTime.parse(json['desired_delivery_time'] as String), + timeDelivered: json['time_delivered'] == null + ? null + : DateTime.parse(json['time_delivered'] as String), +); + +Map _$DeliveryToJson(Delivery instance) => { + 'receipt': instance.receipt, + 'information_for_driver': instance.informationForDriver, + 'desired_delivery_time': instance.desiredDeliveryTime.toIso8601String(), + 'time_delivered': instance.timeDelivered?.toIso8601String(), +}; + +Tour _$TourFromJson(Map json) => Tour( + car: Car.fromJson(json['car'] as Map), + date: json['date'] as String, + deliveries: (json['deliveries'] as List) + .map((e) => Delivery.fromJson(e as Map)) + .toList(), +); + +Map _$TourToJson(Tour instance) => { + 'car': instance.car, + 'date': instance.date, + 'deliveries': instance.deliveries, +}; diff --git a/lib/repository/tour_detail_repository.dart b/lib/repository/tour_detail_repository.dart deleted file mode 100644 index e69de29..0000000 diff --git a/lib/repository/tour_repository.dart b/lib/repository/tour_repository.dart new file mode 100644 index 0000000..7dbaf44 --- /dev/null +++ b/lib/repository/tour_repository.dart @@ -0,0 +1,39 @@ +import 'package:app_gaslieferung/model/supplier.dart'; +import 'package:app_gaslieferung/model/tour.dart'; +import 'package:app_gaslieferung/service/tour_service.dart'; + +import '../model/car.dart'; + +class TourRepository { + TourService service; + + TourRepository({required this.service}); + + /// Lists the possible tours of cars existing for a supplier. + Future getSupplierTourMetadata(String sessionId) async { + final dto = await service.getCars(sessionId); + + await Future.delayed(Duration(seconds: 1)); + + return SupplierTourMetadata( + date: dto.date, + tours: dto.deliveries + .map( + (delivery) => TourMetadata( + car: Car(id: delivery.car.id, carName: delivery.car.carName, driverName: delivery.car.driverName), + amountDeliveries: delivery.amountDeliveries, + ), + ) + .toList(), + ); + } + + /// Lists the possible tours of cars existing for a supplier. + Future getTour(String sessionId, String carId) async { + final tour = await service.getTour(sessionId, carId); + + await Future.delayed(Duration(seconds: 1)); + + return tour; + } +} diff --git a/lib/repository/tour_select_repository.dart b/lib/repository/tour_select_repository.dart deleted file mode 100644 index b3e6457..0000000 --- a/lib/repository/tour_select_repository.dart +++ /dev/null @@ -1 +0,0 @@ -class TourSelectRepository {} \ No newline at end of file diff --git a/lib/service/erp_service.dart b/lib/service/erp_service.dart deleted file mode 100644 index e69de29..0000000 diff --git a/lib/service/tour_service.dart b/lib/service/tour_service.dart new file mode 100644 index 0000000..f38e4fe --- /dev/null +++ b/lib/service/tour_service.dart @@ -0,0 +1,49 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:app_gaslieferung/dto/fail_response_dto.dart'; +import 'package:app_gaslieferung/dto/get_deliveries_dto.dart'; +import 'package:app_gaslieferung/exceptions/login.dart'; +import 'package:app_gaslieferung/exceptions/server.dart'; +import 'package:app_gaslieferung/model/tour.dart'; +import 'package:http/http.dart' as http; + +class TourService { + final String baseUrl; + + TourService({required this.baseUrl}); + + Future getCars(String sessionId) async { + final response = await http.get( + Uri.parse('$baseUrl/cars'), + headers: {'Cookie': 'session_id=$sessionId'}, + ); + + int statusCode = response.statusCode; + if (statusCode == HttpStatus.unauthorized) { + throw LoginUnauthorizedException(); + } else if (statusCode != HttpStatus.ok) { + final dto = FailResponseDTO.fromJson(jsonDecode(response.body)); + throw ServerErrorException(message: dto.message); + } + + return GetDeliveriesDTO.fromJson(jsonDecode(response.body)); + } + + Future getTour(String sessionId, String carId) async { + final response = await http.get( + Uri.parse('$baseUrl/tour/$carId'), + headers: {'Cookie': 'session_id=$sessionId'}, + ); + + int statusCode = response.statusCode; + if (statusCode == HttpStatus.unauthorized) { + throw LoginUnauthorizedException(); + } else if (statusCode != HttpStatus.ok) { + final dto = FailResponseDTO.fromJson(jsonDecode(response.body)); + throw ServerErrorException(message: dto.message); + } + + return Tour.fromJson(jsonDecode(response.body)); + } +} \ No newline at end of file diff --git a/lib/ui/page/login.dart b/lib/ui/page/login.dart index e5e3718..6d5ba1e 100644 --- a/lib/ui/page/login.dart +++ b/lib/ui/page/login.dart @@ -1,4 +1,17 @@ +import 'package:app_gaslieferung/bloc/authentication/auth_bloc.dart'; +import 'package:app_gaslieferung/bloc/authentication/auth_event.dart'; +import 'package:app_gaslieferung/bloc/authentication/auth_state.dart'; +import 'package:app_gaslieferung/bloc/message_wrapper/message_bloc.dart'; +import 'package:app_gaslieferung/bloc/message_wrapper/message_event.dart'; +import 'package:app_gaslieferung/exceptions/login.dart'; +import 'package:app_links/app_links.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:url_launcher/url_launcher.dart'; +import '../../bloc/tour_select/bloc.dart'; +import '../../repository/tour_repository.dart'; +import '../../service/tour_service.dart'; +import '../../util/login.dart'; import 'tour_select.dart'; class LoginPage extends StatefulWidget { @@ -9,46 +22,67 @@ class LoginPage extends StatefulWidget { } class _LoginPageState extends State { - void _onLogin() { - Navigator.pushReplacement(context, MaterialPageRoute(builder: (context) => const TourSelectPage())); - } - @override Widget build(BuildContext context) { return Scaffold( - body: Center( - child: Padding( - padding: EdgeInsets.only( - top: MediaQuery.of(context).size.height * 0.1, - ), - child: Column( - children: [ - Image.asset( - "assets/graphics/bg-supplier-clouds.png", - fit: BoxFit.contain, - ), - Text( - "Willkommen bei\nGaslieferung!", - style: Theme.of(context).textTheme.headlineMedium, - textAlign: TextAlign.center, + body: BlocConsumer( + builder: (context, state) { + return Center( + child: Padding( + padding: EdgeInsets.only( + top: MediaQuery.of(context).size.height * 0.1, ), + child: Column( + children: [ + Image.asset( + "assets/graphics/bg-supplier-clouds.png", + fit: BoxFit.contain, + ), + Text( + "Willkommen bei\nGaslieferung!", + style: Theme.of(context).textTheme.headlineMedium, + textAlign: TextAlign.center, + ), - Text( - "\nMelden Sie sich an, um Ihre Tour zu starten.", - style: Theme.of(context).textTheme.bodySmall, - textAlign: TextAlign.center, - ), + Text( + "\nMelden Sie sich an, um Ihre Tour zu starten.", + style: Theme.of(context).textTheme.bodySmall, + textAlign: TextAlign.center, + ), - Padding( - padding: const EdgeInsets.all(50), - child: FilledButton( - onPressed: _onLogin, - child: Text("Einloggen"), + Padding( + padding: const EdgeInsets.all(50), + child: FilledButton( + onPressed: () { + context.read().add(AuthLoginEvent()); + }, + child: Text("Einloggen"), + ), + ), + ], + ), + ), + ); + }, + listener: (context, state) { + if (state is AuthenticatedState) { + Navigator.pushReplacement( + context, + MaterialPageRoute( + builder: (context) => BlocProvider( + create: (context) => TourSelectBloc( + repository: TourRepository( + service: TourService( + baseUrl: "http://127.0.0.1:3000", + ), + ), + ), + child: TourSelectPage(), ), ), - ], - ), - ), + ); + } + }, ), ); } diff --git a/lib/ui/page/tour.dart b/lib/ui/page/tour.dart index 447e61d..347f978 100644 --- a/lib/ui/page/tour.dart +++ b/lib/ui/page/tour.dart @@ -1,18 +1,425 @@ +import 'package:app_gaslieferung/bloc/authentication/auth_bloc.dart'; +import 'package:app_gaslieferung/bloc/authentication/auth_state.dart'; +import 'package:app_gaslieferung/bloc/tour/tour_bloc.dart'; +import 'package:app_gaslieferung/bloc/tour/tour_event.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../../bloc/tour/tour_state.dart'; +import '../../model/tour.dart'; class TourPage extends StatefulWidget { - const TourPage({super.key}); + final String carId; + + const TourPage({super.key, required this.carId}); @override State createState() => _TourPageState(); } class _TourPageState extends State { + @override + void initState() { + super.initState(); + + var authState = context.read().state as AuthenticatedState; + context.read().add( + TourLoadEvent(carId: "1234", sessionId: authState.sessionId), + ); + } + + Widget _upperInfo(Tour tour) { + return Row( + children: [ + SizedBox( + width: 128, + height: 128, + child: Image.asset( + "assets/graphics/bg-supplier-clouds.png", + fit: BoxFit.contain, + ), + ), + + Padding( + padding: const EdgeInsets.only(left: 15), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Heutige Lieferungen", + style: Theme.of(context).textTheme.titleLarge, + ), + Padding( + padding: const EdgeInsets.only(top: 10), + child: Text( + "Sie haben ${tour.deliveries.length.toString()} Lieferungen für heute", + ), + ), + ], + ), + ), + ], + ); + } + + Widget _progressCard(Tour tour) { + return Padding( + padding: const EdgeInsets.only(left: 12, right: 12, bottom: 10), + child: Card( + color: Theme.of(context).colorScheme.surfaceContainerLowest, + child: Padding( + padding: const EdgeInsets.all(15), + child: Column( + children: [ + Row( + children: [ + SizedBox( + width: 48, + height: 48, + child: Image.asset( + "assets/graphics/truck.png", + fit: BoxFit.contain, + ), + ), + + Padding( + padding: const EdgeInsets.only(left: 10), + child: Text( + "Tour-Fortschritt", + style: Theme.of(context).textTheme.titleMedium, + ), + ), + ], + ), + + Padding( + padding: const EdgeInsets.only(top: 0), + child: Column( + children: [ + Text( + "${tour.progressPercentage.toStringAsFixed(0)}% erledigt", + style: TextStyle(fontWeight: FontWeight.bold), + ), + Padding( + padding: const EdgeInsets.only(top: 8, bottom: 16), + child: LinearProgressIndicator( + value: tour.progress, + color: Theme.of(context).colorScheme.secondary, + ), + ), + ], + ), + ), + + Padding(padding: const EdgeInsets.only(bottom: 15)), + + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Column( + children: [ + Row( + children: [ + Icon( + Icons.check_circle, + color: Theme.of(context).colorScheme.tertiary, + ), + Padding( + padding: const EdgeInsets.only(left: 10), + child: Text( + "Erledigte Lieferungen", + style: Theme.of(context).textTheme.labelSmall, + ), + ), + ], + ), + + Text( + "${tour.amountFinishedDeliveries} von ${tour.amountDeliveries}", + style: TextStyle( + color: Theme.of(context).colorScheme.secondary, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + + const VerticalDivider( + width: 20, + thickness: 1, + indent: 20, + endIndent: 1, + color: Colors.black, + ), + + Expanded( + child: Column( + children: [ + Row( + children: [ + Icon( + Icons.event_note_outlined, + color: Theme.of(context).colorScheme.primary, + ), + Padding( + padding: const EdgeInsets.only(left: 10), + child: Text( + "Offene Lieferungen", + style: Theme.of(context).textTheme.labelSmall, + ), + ), + ], + ), + + Text( + "${tour.amountDeliveriesLeft} übrig", + style: TextStyle( + color: Theme.of(context).colorScheme.primary, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + ], + ), + ], + ), + ), + ), + ); + } + + Widget _nextDeliveryCard(Tour tour) { + return Padding( + padding: const EdgeInsets.only(left: 10, right: 10, bottom: 10), + child: Card( + color: Theme.of(context).colorScheme.surfaceContainer, + child: Padding( + padding: const EdgeInsets.all(15), + child: Padding( + padding: const EdgeInsets.all(0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Nächste Lieferung", + style: Theme.of(context).textTheme.titleMedium, + ), + + SizedBox( + width: 48, + height: 48, + child: Image.asset( + "assets/graphics/location-pin-cloud.png", + ), + ), + ], + ), + + Padding( + padding: const EdgeInsets.only(top: 0), + child: SizedBox( + width: double.infinity, + child: Card( + color: Theme.of( + context, + ).colorScheme.surfaceContainerLowest, + child: Padding( + padding: const EdgeInsets.all(15), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + tour.deliveries[0].receipt.customer.name, + style: Theme.of( + context, + ).textTheme.titleSmall, + ), + Text( + tour + .deliveries[0] + .receipt + .customer + .displayAddress, + ), + ], + ), + + IconButton( + onPressed: () {}, + icon: Icon(Icons.map, size: 32), + color: Theme.of( + context, + ).colorScheme.secondary, + ), + ], + ), + + Padding( + padding: const EdgeInsets.only( + top: 10, + bottom: 10, + ), + child: const Divider(), + ), + + Row( + children: [ + Row( + children: [ + Icon( + Icons.location_pin, + color: Theme.of( + context, + ).colorScheme.secondary, + ), + Padding( + padding: const EdgeInsets.only(left: 2), + child: Text("10km"), + ), + ], + ), + + Padding( + padding: const EdgeInsets.only(left: 15), + child: Row( + children: [ + Icon( + Icons.access_time_filled, + color: Theme.of( + context, + ).colorScheme.primary, + ), + Padding( + padding: const EdgeInsets.only(left: 2), + child: Text("Ankunft in 10min"), + ), + ], + ), + ), + ], + ), + ], + ), + ), + ), + ), + ), + ], + ), + ), + ), + ), + ); + } + + Widget _openDeliveries(Tour tour) { + return Padding( + padding: const EdgeInsets.only(left: 10, right: 10), + child: Card( + color: Theme.of(context).colorScheme.surfaceContainer, + child: Padding( + padding: const EdgeInsets.all(15), + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "Lieferungen", + style: Theme.of(context).textTheme.titleMedium, + ), + ], + ), + + Padding( + padding: const EdgeInsets.only(top: 10), + child: SizedBox( + width: double.infinity, + child: ListView.separated( + shrinkWrap: true, + physics: NeverScrollableScrollPhysics(), + itemBuilder: (context, index) { + final delivery = tour.deliveries[index]; + return ListTile( + leading: SizedBox( + width: 32, + height: 32, + child: Image.asset("assets/graphics/gas-tank.png"), + ), + tileColor: Theme.of( + context, + ).colorScheme.surfaceContainerLowest, + title: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + delivery.receipt.customer.name, + style: TextStyle(fontWeight: FontWeight.bold), + ), + Text(delivery.receipt.customer.displayAddress, style: TextStyle(fontSize: 13)), + ], + ), + trailing: Icon(Icons.arrow_forward_ios), + ); + }, + separatorBuilder: (context, index) => + Padding(padding: const EdgeInsets.only(top: 10)), + itemCount: tour.amountDeliveries, + ), + ), + ), + ], + ), + ), + ), + ); + } + + Widget _listTour(Tour tour) { + return ListView( + children: [ + _upperInfo(tour), + _progressCard(tour), + _nextDeliveryCard(tour), + _openDeliveries(tour), + ], + ); + } + @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text("Lieferungen")), - body: Center(child: const Text("HI")), + body: BlocBuilder( + builder: (context, state) { + if (state is TourLoading) { + return Center(child: CircularProgressIndicator()); + } + + if (state is TourLoaded) { + return _listTour(state.tour); + } + + if (state is TourLoadingFailed) { + return Center(child: Text(state.message)); + } + + return Container(); + }, + ), ); } } diff --git a/lib/ui/page/tour_detail.dart b/lib/ui/page/tour_detail.dart new file mode 100644 index 0000000..ea6c1dd --- /dev/null +++ b/lib/ui/page/tour_detail.dart @@ -0,0 +1,19 @@ +import 'package:flutter/material.dart'; + +class TourDetailPage extends StatefulWidget { + /// The id of the car that has been chosen a step before. + final String supplierCarId; + + const TourDetailPage({super.key, required this.supplierCarId}); + + @override + State createState() => _TourDetailPageState(); +} + +class _TourDetailPageState extends State { + @override + Widget build(BuildContext context) { + // TODO: implement build + throw UnimplementedError(); + } +} \ No newline at end of file diff --git a/lib/ui/page/tour_select.dart b/lib/ui/page/tour_select.dart index 9d0bb7d..652c4d9 100644 --- a/lib/ui/page/tour_select.dart +++ b/lib/ui/page/tour_select.dart @@ -1,4 +1,13 @@ +import 'package:app_gaslieferung/bloc/authentication/auth_bloc.dart'; +import 'package:app_gaslieferung/bloc/authentication/auth_state.dart'; +import 'package:app_gaslieferung/bloc/tour_select/bloc.dart'; +import 'package:app_gaslieferung/model/supplier.dart'; +import 'package:app_gaslieferung/ui/page/tour.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../../bloc/tour_select/event.dart'; +import '../../bloc/tour_select/state.dart'; class TourSelectPage extends StatefulWidget { const TourSelectPage({super.key}); @@ -8,95 +17,156 @@ class TourSelectPage extends StatefulWidget { } class _TourSelectPageState extends State { - Widget _listTour() { + @override + void initState() { + super.initState(); + + context.read().add( + TourSelectLoadMetadataEvent( + sessionId: + (context.read().state as AuthenticatedState).sessionId, + ), + ); + } + + Widget _listTour(SupplierTourMetadata data) { return ListView.separated( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), itemBuilder: (context, index) { + TourMetadata tourData = data.tours[index]; + return ListTile( leading: Icon(Icons.local_shipping_outlined), - title: const Text("Dennis Nemec"), - subtitle: const Text("15 Lieferungen"), + onTap: () => Navigator.push( + context, + MaterialPageRoute( + builder: (context) => TourPage(carId: tourData.car.id.toString()), + ), + ), + title: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [Text(tourData.car.driverName ?? tourData.car.carName)], + ), + subtitle: Row( + children: [ + tourData.car.driverName != null + ? Text("${tourData.car.carName} | ") + : Container(), + + Text("${tourData.amountDeliveries} Lieferungen"), + ], + ), tileColor: Theme.of(context).colorScheme.surface, trailing: Icon(Icons.arrow_forward_ios), ); }, separatorBuilder: (BuildContext context, int index) => SizedBox(height: 10), - itemCount: 6, + itemCount: data.tours.length, ); } @override Widget build(BuildContext context) { return Scaffold( - body: Center( - child: ListView( - children: [ - Image.asset( - "assets/graphics/bg-carrier-cylinder-duo.png", - fit: BoxFit.contain, - ), + body: BlocBuilder( + builder: (context, state) { + if (state is TourSelectLoading) { + return const Center(child: CircularProgressIndicator()); + } - Padding(padding: const EdgeInsets.only(top: 25), child: Text( - "Wählen Sie Ihre Tour aus:", - style: Theme.of(context).textTheme.titleLarge, - textAlign: TextAlign.center, - ),), + if (state is TourSelectError) { + return Center(child: Text(state.message)); + } - Padding( - padding: const EdgeInsets.all(15), - child: SizedBox( - width: double.infinity, - child: Card( - color: Theme.of(context).colorScheme.surfaceContainerHighest, - child: Padding( - padding: const EdgeInsets.all(20), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - Padding( - padding: const EdgeInsets.only(right: 10), - child: Icon(Icons.tour), - ), - Text( - "Touren", - style: Theme.of(context).textTheme.titleMedium, - ), - ], - ), - Row( - children: [ - Padding( - padding: const EdgeInsets.only(right: 10), - child: Icon(Icons.calendar_month), - ), - Text( - "01.02.2026", - style: Theme.of(context).textTheme.labelLarge, - ), - ], - ), - ], - ), + if (state is TourSelectLoaded) { + return Center( + child: ListView( + children: [ + Image.asset( + "assets/graphics/bg-carrier-cylinder-duo.png", + fit: BoxFit.contain, + ), - Padding( - padding: const EdgeInsets.only(top: 15), - child: _listTour(), - ), - ], + Padding( + padding: const EdgeInsets.only(top: 25), + child: Text( + "Wählen Sie Ihre Tour aus:", + style: Theme.of(context).textTheme.titleLarge, + textAlign: TextAlign.center, ), ), - ), + + Padding( + padding: const EdgeInsets.all(15), + child: SizedBox( + width: double.infinity, + child: Card( + color: Theme.of( + context, + ).colorScheme.surfaceContainerHighest, + child: Padding( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Padding( + padding: const EdgeInsets.only( + right: 10, + ), + child: Icon(Icons.tour), + ), + Text( + "Touren", + style: Theme.of( + context, + ).textTheme.titleMedium, + ), + ], + ), + Row( + children: [ + Padding( + padding: const EdgeInsets.only( + right: 10, + ), + child: Icon(Icons.calendar_month), + ), + Text( + state.data.date, + style: Theme.of( + context, + ).textTheme.labelLarge, + ), + ], + ), + ], + ), + + Padding( + padding: const EdgeInsets.only(top: 15), + child: _listTour(state.data), + ), + ], + ), + ), + ), + ), + ), + ], ), - ), - ], - ), + ); + } + + return Container(); + }, ), ); } diff --git a/lib/ui/theme/theme.dart b/lib/ui/theme/theme.dart index 7d4b1c5..b12d196 100644 --- a/lib/ui/theme/theme.dart +++ b/lib/ui/theme/theme.dart @@ -29,9 +29,14 @@ class AppTheme { onErrorContainer: Color(0xFF410E0B), surface: Color(0xFFFFF8F2), + surfaceContainerLowest: Color(0xFFFFFFFF), + surfaceContainerLow: Color(0xFFFFF4EB), + surfaceContainer: Color(0xFFFAEFE5), + surfaceContainerHigh: Color(0xFFF5E9DE), + surfaceContainerHighest: Color(0xFFEFE3D8), onSurface: Color(0xFF1F1F1F), - surfaceContainerHighest: Color(0xFFF2E6DA), + //surfaceContainerHighest: Color(0xFFF2E6DA), onSurfaceVariant: Color(0xFF4A4A4A), outline: Color(0xFFC8BEB4), diff --git a/lib/ui/widgets/message_wrapper/message_wrapper.dart b/lib/ui/widgets/message_wrapper/message_wrapper.dart new file mode 100644 index 0000000..def05bf --- /dev/null +++ b/lib/ui/widgets/message_wrapper/message_wrapper.dart @@ -0,0 +1,70 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../../../bloc/message_wrapper/message_bloc.dart'; +import '../../../bloc/message_wrapper/message_state.dart'; + +class MessageWrapperWidget extends StatefulWidget { + final Widget child; + + const MessageWrapperWidget({super.key, required this.child}); + + @override + State createState() => MessageWrapperWidgetState(); + + static MessageWrapperWidgetState of(BuildContext context) { + return context.findAncestorStateOfType()!; + } +} + +class MessageWrapperWidgetState extends State { + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + if (state is MessageShowState) { + return Stack( + children: [ + widget.child, + Positioned( + top: 50, + left: 0, + right: 0, + child: Padding( + padding: const EdgeInsets.all(10), + child: Material( + color: Colors.transparent, + child: Container( + margin: EdgeInsets.all(8), + padding: EdgeInsets.all(16), + decoration: BoxDecoration( + color: Theme.of( + context, + ).colorScheme.primaryContainer, + borderRadius: BorderRadius.circular(8), + ), + child: Padding( + padding: const EdgeInsets.all(10), + child: Row( + children: [ + Padding( + padding: const EdgeInsets.only(right: 15), + child: Icon(Icons.info, size: 32), + ), + Expanded(child: Text(state.message)), + ], + ), + ), + ), + ), + ), + ), + ], + ); + } + + return widget.child; + }, + ); + } +} diff --git a/lib/util/login.dart b/lib/util/login.dart new file mode 100644 index 0000000..aa8a1b6 --- /dev/null +++ b/lib/util/login.dart @@ -0,0 +1,15 @@ +import 'package:app_gaslieferung/exceptions/login.dart'; +import 'package:app_links/app_links.dart'; + +String getSessionIdFromUrl(Uri uri) { + if (uri.scheme == 'myapp' && uri.host == 'callback') { + final code = uri.queryParameters['session_id']; + if (code != null) { + return code; + } else { + throw LoginNoSessionIdException(); + } + } + + throw LoginInvalidUrlException(); +} diff --git a/pubspec.yaml b/pubspec.yaml index d80f474..bcfb678 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -35,6 +35,11 @@ dependencies: # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.8 bloc: ^9.2.0 + app_links: ^7.0.0 + url_launcher: ^6.3.2 + flutter_bloc: ^9.1.1 + http: ^1.6.0 + json_serializable: ^6.12.0 dev_dependencies: flutter_test: @@ -46,6 +51,7 @@ dev_dependencies: # package. See that file for information about deactivating specific lint # rules and activating additional ones. flutter_lints: ^6.0.0 + build_runner: ^2.10.5 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec @@ -62,10 +68,11 @@ flutter: assets: - assets/graphics/bg-supplier-clouds.png - assets/graphics/gas-tank.png + - assets/graphics/gas-carrier.png - assets/graphics/supplier.png - assets/graphics/location-pin-cloud.png - assets/graphics/bg-carrier-cylinder-duo.png - + - assets/graphics/truck.png # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/to/resolution-aware-images diff --git a/test/widget_test.dart b/test/widget_test.dart index 2fc882d..15069a3 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -13,7 +13,7 @@ import 'package:app_gaslieferung/main.dart'; void main() { testWidgets('Counter increments smoke test', (WidgetTester tester) async { // Build our app and trigger a frame. - await tester.pumpWidget(const MyApp()); + await tester.pumpWidget(const GasDeliveryApp()); // Verify that our counter starts at 0. expect(find.text('0'), findsOneWidget);