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);