6177214e-ce7c-49e3-99de-ff9721b26f63 — Commit 06eaec02
Changed files
concierge/example/pubspec.lock | 32 ++++ .../presentation/app/cart_cubit.freezed.dart | 52 +++--- .../bloc/product_details_state.freezed.dart | 52 +++--- concierge/lib/concierge_route.dart | 4 +- concierge/lib/presentation/app/cart_cubit.dart | 52 ++++-- .../transitions/slide_up_transition.dart | 2 +- .../screens/confirm_order/confirm_order_route.dart | 4 +- .../confirm_order/confirm_order_screen.dart | 19 ++- .../hotel_overview_page_screen.dart | 15 +- .../hotel_overview_page/widgets/cart_app_bar.dart | 69 ++++++++ .../widgets/product_list_tile.dart | 82 ++++++---- .../screens/payment/payment_route.dart | 4 +- .../screens/payment/payment_screen.dart | 8 + .../bloc/product_details_cubit.dart | 76 ++++++--- .../bloc/product_details_state.dart | 4 +- .../product_details/product_details_route.dart | 6 +- .../product_details/product_details_screen.dart | 180 +++++++++++++++++---- .../widgets/product_details_app_bar.dart | 69 ++++++++ .../provide_location/provide_location_route.dart | 4 +- .../provide_location/provide_location_screen.dart | 19 ++- .../screens/receipt/receipt_route.dart | 4 +- .../screens/receipt/receipt_screen.dart | 7 + .../screens/review_order/review_order_screen.dart | 4 + concierge/pubspec.yaml | 1 + 24 files changed, 593 insertions(+), 176 deletions(-)
Diff
diff --git a/concierge/example/pubspec.lock b/concierge/example/pubspec.lock
index 9841a967..0b5b729f 100644
--- a/concierge/example/pubspec.lock
+++ b/concierge/example/pubspec.lock
@@ -128,6 +128,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.0.7"
+ csslib:
+ dependency: transitive
+ description:
+ name: csslib
+ sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.0.2"
cupertino_icons:
dependency: "direct main"
description:
@@ -234,6 +242,14 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
+ flutter_html:
+ dependency: transitive
+ description:
+ name: flutter_html
+ sha256: "38a2fd702ffdf3243fb7441ab58aa1bc7e6922d95a50db76534de8260638558d"
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.0.0"
flutter_lints:
dependency: "direct dev"
description:
@@ -374,6 +390,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.1.2"
+ html:
+ dependency: transitive
+ description:
+ name: html
+ sha256: "6d1264f2dffa1b1101c25a91dff0dc2daee4c18e87cd8538729773c073dbf602"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.15.6"
http:
dependency: transitive
description:
@@ -451,6 +475,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "5.1.1"
+ list_counter:
+ dependency: transitive
+ description:
+ name: list_counter
+ sha256: c447ae3dfcd1c55f0152867090e67e219d42fe6d4f2807db4bbe8b8d69912237
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.0.2"
logging:
dependency: transitive
description:
diff --git a/concierge/lib/_generated/presentation/app/cart_cubit.freezed.dart b/concierge/lib/_generated/presentation/app/cart_cubit.freezed.dart
index 7f71bf4e..0a84501c 100644
--- a/concierge/lib/_generated/presentation/app/cart_cubit.freezed.dart
+++ b/concierge/lib/_generated/presentation/app/cart_cubit.freezed.dart
@@ -14,7 +14,7 @@ T _$identity<T>(T value) => value;
/// @nodoc
mixin _$CartState {
- bool get isLoading; AppError get error; List<Product> get productsInCart;
+ bool get isLoading; AppError get error; Map<int, ProductInCart> get productsWithQuantity;
/// Create a copy of CartState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@@ -25,16 +25,16 @@ $CartStateCopyWith<CartState> get copyWith => _$CartStateCopyWithImpl<CartState>
@override
bool operator ==(Object other) {
- return identical(this, other) || (other.runtimeType == runtimeType&&other is CartState&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.error, error) || other.error == error)&&const DeepCollectionEquality().equals(other.productsInCart, productsInCart));
+ return identical(this, other) || (other.runtimeType == runtimeType&&other is CartState&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.error, error) || other.error == error)&&const DeepCollectionEquality().equals(other.productsWithQuantity, productsWithQuantity));
}
@override
-int get hashCode => Object.hash(runtimeType,isLoading,error,const DeepCollectionEquality().hash(productsInCart));
+int get hashCode => Object.hash(runtimeType,isLoading,error,const DeepCollectionEquality().hash(productsWithQuantity));
@override
String toString() {
- return 'CartState(isLoading: $isLoading, error: $error, productsInCart: $productsInCart)';
+ return 'CartState(isLoading: $isLoading, error: $error, productsWithQuantity: $productsWithQuantity)';
}
@@ -45,7 +45,7 @@ abstract mixin class $CartStateCopyWith<$Res> {
factory $CartStateCopyWith(CartState value, $Res Function(CartState) _then) = _$CartStateCopyWithImpl;
@useResult
$Res call({
- bool isLoading, AppError error, List<Product> productsInCart
+ bool isLoading, AppError error, Map<int, ProductInCart> productsWithQuantity
});
@@ -62,12 +62,12 @@ class _$CartStateCopyWithImpl<$Res>
/// Create a copy of CartState
/// with the given fields replaced by the non-null parameter values.
-@pragma('vm:prefer-inline') @override $Res call({Object? isLoading = null,Object? error = null,Object? productsInCart = null,}) {
+@pragma('vm:prefer-inline') @override $Res call({Object? isLoading = null,Object? error = null,Object? productsWithQuantity = null,}) {
return _then(_self.copyWith(
isLoading: null == isLoading ? _self.isLoading : isLoading // ignore: cast_nullable_to_non_nullable
as bool,error: null == error ? _self.error : error // ignore: cast_nullable_to_non_nullable
-as AppError,productsInCart: null == productsInCart ? _self.productsInCart : productsInCart // ignore: cast_nullable_to_non_nullable
-as List<Product>,
+as AppError,productsWithQuantity: null == productsWithQuantity ? _self.productsWithQuantity : productsWithQuantity // ignore: cast_nullable_to_non_nullable
+as Map<int, ProductInCart>,
));
}
@@ -152,10 +152,10 @@ return $default(_that);case _:
/// }
/// ```
-@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( bool isLoading, AppError error, List<Product> productsInCart)? $default,{required TResult orElse(),}) {final _that = this;
+@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( bool isLoading, AppError error, Map<int, ProductInCart> productsWithQuantity)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _CartState() when $default != null:
-return $default(_that.isLoading,_that.error,_that.productsInCart);case _:
+return $default(_that.isLoading,_that.error,_that.productsWithQuantity);case _:
return orElse();
}
@@ -173,10 +173,10 @@ return $default(_that.isLoading,_that.error,_that.productsInCart);case _:
/// }
/// ```
-@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( bool isLoading, AppError error, List<Product> productsInCart) $default,) {final _that = this;
+@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( bool isLoading, AppError error, Map<int, ProductInCart> productsWithQuantity) $default,) {final _that = this;
switch (_that) {
case _CartState():
-return $default(_that.isLoading,_that.error,_that.productsInCart);case _:
+return $default(_that.isLoading,_that.error,_that.productsWithQuantity);case _:
throw StateError('Unexpected subclass');
}
@@ -193,10 +193,10 @@ return $default(_that.isLoading,_that.error,_that.productsInCart);case _:
/// }
/// ```
-@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( bool isLoading, AppError error, List<Product> productsInCart)? $default,) {final _that = this;
+@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( bool isLoading, AppError error, Map<int, ProductInCart> productsWithQuantity)? $default,) {final _that = this;
switch (_that) {
case _CartState() when $default != null:
-return $default(_that.isLoading,_that.error,_that.productsInCart);case _:
+return $default(_that.isLoading,_that.error,_that.productsWithQuantity);case _:
return null;
}
@@ -208,16 +208,16 @@ return $default(_that.isLoading,_that.error,_that.productsInCart);case _:
class _CartState extends CartState {
- const _CartState({this.isLoading = false, this.error = AppError.none, final List<Product> productsInCart = const []}): _productsInCart = productsInCart,super._();
+ const _CartState({this.isLoading = false, this.error = AppError.none, final Map<int, ProductInCart> productsWithQuantity = const {}}): _productsWithQuantity = productsWithQuantity,super._();
@override@JsonKey() final bool isLoading;
@override@JsonKey() final AppError error;
- final List<Product> _productsInCart;
-@override@JsonKey() List<Product> get productsInCart {
- if (_productsInCart is EqualUnmodifiableListView) return _productsInCart;
+ final Map<int, ProductInCart> _productsWithQuantity;
+@override@JsonKey() Map<int, ProductInCart> get productsWithQuantity {
+ if (_productsWithQuantity is EqualUnmodifiableMapView) return _productsWithQuantity;
// ignore: implicit_dynamic_type
- return EqualUnmodifiableListView(_productsInCart);
+ return EqualUnmodifiableMapView(_productsWithQuantity);
}
@@ -231,16 +231,16 @@ _$CartStateCopyWith<_CartState> get copyWith => __$CartStateCopyWithImpl<_CartSt
@override
bool operator ==(Object other) {
- return identical(this, other) || (other.runtimeType == runtimeType&&other is _CartState&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.error, error) || other.error == error)&&const DeepCollectionEquality().equals(other._productsInCart, _productsInCart));
+ return identical(this, other) || (other.runtimeType == runtimeType&&other is _CartState&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.error, error) || other.error == error)&&const DeepCollectionEquality().equals(other._productsWithQuantity, _productsWithQuantity));
}
@override
-int get hashCode => Object.hash(runtimeType,isLoading,error,const DeepCollectionEquality().hash(_productsInCart));
+int get hashCode => Object.hash(runtimeType,isLoading,error,const DeepCollectionEquality().hash(_productsWithQuantity));
@override
String toString() {
- return 'CartState(isLoading: $isLoading, error: $error, productsInCart: $productsInCart)';
+ return 'CartState(isLoading: $isLoading, error: $error, productsWithQuantity: $productsWithQuantity)';
}
@@ -251,7 +251,7 @@ abstract mixin class _$CartStateCopyWith<$Res> implements $CartStateCopyWith<$Re
factory _$CartStateCopyWith(_CartState value, $Res Function(_CartState) _then) = __$CartStateCopyWithImpl;
@override @useResult
$Res call({
- bool isLoading, AppError error, List<Product> productsInCart
+ bool isLoading, AppError error, Map<int, ProductInCart> productsWithQuantity
});
@@ -268,12 +268,12 @@ class __$CartStateCopyWithImpl<$Res>
/// Create a copy of CartState
/// with the given fields replaced by the non-null parameter values.
-@override @pragma('vm:prefer-inline') $Res call({Object? isLoading = null,Object? error = null,Object? productsInCart = null,}) {
+@override @pragma('vm:prefer-inline') $Res call({Object? isLoading = null,Object? error = null,Object? productsWithQuantity = null,}) {
return _then(_CartState(
isLoading: null == isLoading ? _self.isLoading : isLoading // ignore: cast_nullable_to_non_nullable
as bool,error: null == error ? _self.error : error // ignore: cast_nullable_to_non_nullable
-as AppError,productsInCart: null == productsInCart ? _self._productsInCart : productsInCart // ignore: cast_nullable_to_non_nullable
-as List<Product>,
+as AppError,productsWithQuantity: null == productsWithQuantity ? _self._productsWithQuantity : productsWithQuantity // ignore: cast_nullable_to_non_nullable
+as Map<int, ProductInCart>,
));
}
diff --git a/concierge/lib/_generated/presentation/screens/product_details/bloc/product_details_state.freezed.dart b/concierge/lib/_generated/presentation/screens/product_details/bloc/product_details_state.freezed.dart
index 4358b96e..19ed9185 100644
--- a/concierge/lib/_generated/presentation/screens/product_details/bloc/product_details_state.freezed.dart
+++ b/concierge/lib/_generated/presentation/screens/product_details/bloc/product_details_state.freezed.dart
@@ -14,7 +14,7 @@ T _$identity<T>(T value) => value;
/// @nodoc
mixin _$ProductDetailsState {
- bool get isLoading; AppError get error; Product? get product;
+ bool get isLoading; AppError get error; int get quantity; Map<int, int> get selectedVariant; Product? get product;
/// Create a copy of ProductDetailsState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@@ -25,16 +25,16 @@ $ProductDetailsStateCopyWith<ProductDetailsState> get copyWith => _$ProductDetai
@override
bool operator ==(Object other) {
- return identical(this, other) || (other.runtimeType == runtimeType&&other is ProductDetailsState&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.error, error) || other.error == error)&&(identical(other.product, product) || other.product == product));
+ return identical(this, other) || (other.runtimeType == runtimeType&&other is ProductDetailsState&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.error, error) || other.error == error)&&(identical(other.quantity, quantity) || other.quantity == quantity)&&const DeepCollectionEquality().equals(other.selectedVariant, selectedVariant)&&(identical(other.product, product) || other.product == product));
}
@override
-int get hashCode => Object.hash(runtimeType,isLoading,error,product);
+int get hashCode => Object.hash(runtimeType,isLoading,error,quantity,const DeepCollectionEquality().hash(selectedVariant),product);
@override
String toString() {
- return 'ProductDetailsState(isLoading: $isLoading, error: $error, product: $product)';
+ return 'ProductDetailsState(isLoading: $isLoading, error: $error, quantity: $quantity, selectedVariant: $selectedVariant, product: $product)';
}
@@ -45,7 +45,7 @@ abstract mixin class $ProductDetailsStateCopyWith<$Res> {
factory $ProductDetailsStateCopyWith(ProductDetailsState value, $Res Function(ProductDetailsState) _then) = _$ProductDetailsStateCopyWithImpl;
@useResult
$Res call({
- bool isLoading, AppError error, Product? product
+ bool isLoading, AppError error, int quantity, Map<int, int> selectedVariant, Product? product
});
@@ -62,11 +62,13 @@ class _$ProductDetailsStateCopyWithImpl<$Res>
/// Create a copy of ProductDetailsState
/// with the given fields replaced by the non-null parameter values.
-@pragma('vm:prefer-inline') @override $Res call({Object? isLoading = null,Object? error = null,Object? product = freezed,}) {
+@pragma('vm:prefer-inline') @override $Res call({Object? isLoading = null,Object? error = null,Object? quantity = null,Object? selectedVariant = null,Object? product = freezed,}) {
return _then(_self.copyWith(
isLoading: null == isLoading ? _self.isLoading : isLoading // ignore: cast_nullable_to_non_nullable
as bool,error: null == error ? _self.error : error // ignore: cast_nullable_to_non_nullable
-as AppError,product: freezed == product ? _self.product : product // ignore: cast_nullable_to_non_nullable
+as AppError,quantity: null == quantity ? _self.quantity : quantity // ignore: cast_nullable_to_non_nullable
+as int,selectedVariant: null == selectedVariant ? _self.selectedVariant : selectedVariant // ignore: cast_nullable_to_non_nullable
+as Map<int, int>,product: freezed == product ? _self.product : product // ignore: cast_nullable_to_non_nullable
as Product?,
));
}
@@ -152,10 +154,10 @@ return $default(_that);case _:
/// }
/// ```
-@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( bool isLoading, AppError error, Product? product)? $default,{required TResult orElse(),}) {final _that = this;
+@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( bool isLoading, AppError error, int quantity, Map<int, int> selectedVariant, Product? product)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _ProductDetailsState() when $default != null:
-return $default(_that.isLoading,_that.error,_that.product);case _:
+return $default(_that.isLoading,_that.error,_that.quantity,_that.selectedVariant,_that.product);case _:
return orElse();
}
@@ -173,10 +175,10 @@ return $default(_that.isLoading,_that.error,_that.product);case _:
/// }
/// ```
-@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( bool isLoading, AppError error, Product? product) $default,) {final _that = this;
+@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( bool isLoading, AppError error, int quantity, Map<int, int> selectedVariant, Product? product) $default,) {final _that = this;
switch (_that) {
case _ProductDetailsState():
-return $default(_that.isLoading,_that.error,_that.product);case _:
+return $default(_that.isLoading,_that.error,_that.quantity,_that.selectedVariant,_that.product);case _:
throw StateError('Unexpected subclass');
}
@@ -193,10 +195,10 @@ return $default(_that.isLoading,_that.error,_that.product);case _:
/// }
/// ```
-@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( bool isLoading, AppError error, Product? product)? $default,) {final _that = this;
+@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( bool isLoading, AppError error, int quantity, Map<int, int> selectedVariant, Product? product)? $default,) {final _that = this;
switch (_that) {
case _ProductDetailsState() when $default != null:
-return $default(_that.isLoading,_that.error,_that.product);case _:
+return $default(_that.isLoading,_that.error,_that.quantity,_that.selectedVariant,_that.product);case _:
return null;
}
@@ -208,11 +210,19 @@ return $default(_that.isLoading,_that.error,_that.product);case _:
class _ProductDetailsState extends ProductDetailsState {
- const _ProductDetailsState({this.isLoading = false, this.error = AppError.none, this.product}): super._();
+ const _ProductDetailsState({this.isLoading = false, this.error = AppError.none, this.quantity = 0, final Map<int, int> selectedVariant = const {}, this.product}): _selectedVariant = selectedVariant,super._();
@override@JsonKey() final bool isLoading;
@override@JsonKey() final AppError error;
+@override@JsonKey() final int quantity;
+ final Map<int, int> _selectedVariant;
+@override@JsonKey() Map<int, int> get selectedVariant {
+ if (_selectedVariant is EqualUnmodifiableMapView) return _selectedVariant;
+ // ignore: implicit_dynamic_type
+ return EqualUnmodifiableMapView(_selectedVariant);
+}
+
@override final Product? product;
/// Create a copy of ProductDetailsState
@@ -225,16 +235,16 @@ _$ProductDetailsStateCopyWith<_ProductDetailsState> get copyWith => __$ProductDe
@override
bool operator ==(Object other) {
- return identical(this, other) || (other.runtimeType == runtimeType&&other is _ProductDetailsState&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.error, error) || other.error == error)&&(identical(other.product, product) || other.product == product));
+ return identical(this, other) || (other.runtimeType == runtimeType&&other is _ProductDetailsState&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.error, error) || other.error == error)&&(identical(other.quantity, quantity) || other.quantity == quantity)&&const DeepCollectionEquality().equals(other._selectedVariant, _selectedVariant)&&(identical(other.product, product) || other.product == product));
}
@override
-int get hashCode => Object.hash(runtimeType,isLoading,error,product);
+int get hashCode => Object.hash(runtimeType,isLoading,error,quantity,const DeepCollectionEquality().hash(_selectedVariant),product);
@override
String toString() {
- return 'ProductDetailsState(isLoading: $isLoading, error: $error, product: $product)';
+ return 'ProductDetailsState(isLoading: $isLoading, error: $error, quantity: $quantity, selectedVariant: $selectedVariant, product: $product)';
}
@@ -245,7 +255,7 @@ abstract mixin class _$ProductDetailsStateCopyWith<$Res> implements $ProductDeta
factory _$ProductDetailsStateCopyWith(_ProductDetailsState value, $Res Function(_ProductDetailsState) _then) = __$ProductDetailsStateCopyWithImpl;
@override @useResult
$Res call({
- bool isLoading, AppError error, Product? product
+ bool isLoading, AppError error, int quantity, Map<int, int> selectedVariant, Product? product
});
@@ -262,11 +272,13 @@ class __$ProductDetailsStateCopyWithImpl<$Res>
/// Create a copy of ProductDetailsState
/// with the given fields replaced by the non-null parameter values.
-@override @pragma('vm:prefer-inline') $Res call({Object? isLoading = null,Object? error = null,Object? product = freezed,}) {
+@override @pragma('vm:prefer-inline') $Res call({Object? isLoading = null,Object? error = null,Object? quantity = null,Object? selectedVariant = null,Object? product = freezed,}) {
return _then(_ProductDetailsState(
isLoading: null == isLoading ? _self.isLoading : isLoading // ignore: cast_nullable_to_non_nullable
as bool,error: null == error ? _self.error : error // ignore: cast_nullable_to_non_nullable
-as AppError,product: freezed == product ? _self.product : product // ignore: cast_nullable_to_non_nullable
+as AppError,quantity: null == quantity ? _self.quantity : quantity // ignore: cast_nullable_to_non_nullable
+as int,selectedVariant: null == selectedVariant ? _self._selectedVariant : selectedVariant // ignore: cast_nullable_to_non_nullable
+as Map<int, int>,product: freezed == product ? _self.product : product // ignore: cast_nullable_to_non_nullable
as Product?,
));
}
diff --git a/concierge/lib/concierge_route.dart b/concierge/lib/concierge_route.dart
index 9ebe9746..2a202577 100644
--- a/concierge/lib/concierge_route.dart
+++ b/concierge/lib/concierge_route.dart
@@ -14,7 +14,6 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:go_router/go_router.dart';
import 'package:pretty_dio_logger/pretty_dio_logger.dart';
-
import 'data/local/secure_storage/concierge_secure_storage.dart';
import 'data/remote/api/concierge_interceptor.dart';
import 'data/remote/api/concierge_service.dart';
@@ -24,6 +23,7 @@ import 'flavors.dart';
final conciergeShellRoute = StatefulShellRoute.indexedStack(
redirect: (context, state) {
print("qqq concierge: ${state.matchedLocation}");
+ if (state.matchedLocation == "/exit") context.pop();
return null;
},
branches: [
@@ -77,7 +77,7 @@ final conciergeShellRoute = StatefulShellRoute.indexedStack(
child: MultiBlocProvider(
providers: [
BlocProvider(create: (context) => ConciergeCubit(context.read())),
- BlocProvider(create: (context) => CartCubit()),
+ BlocProvider(create: (context) => CartCubit(context.read())),
],
child: child,
),
diff --git a/concierge/lib/presentation/app/cart_cubit.dart b/concierge/lib/presentation/app/cart_cubit.dart
index 42e7e290..7acd1191 100644
--- a/concierge/lib/presentation/app/cart_cubit.dart
+++ b/concierge/lib/presentation/app/cart_cubit.dart
@@ -1,28 +1,46 @@
-import 'package:concierge/presentation/base/base_cubit.dart';
import 'package:concierge/data/remote/models/product.dart';
+import 'package:concierge/domain/models/data_state.dart';
+import 'package:concierge/presentation/app/concierge_cubit.dart';
+import 'package:concierge/presentation/base/base_cubit.dart';
import 'package:concierge/domain/models/app_error.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part '../../_generated/presentation/app/cart_cubit.freezed.dart';
class CartCubit extends BaseCubit<CartState> {
- CartCubit() : super(const CartState());
+ final ConciergeCubit _conciergeCubit;
- void addToCart(Product product) {
- final copy = List.of(state.productsInCart);
- copy.add(product);
- safeEmit(state.copyWith(productsInCart: copy));
+ CartCubit(this._conciergeCubit) : super(const CartState());
+
+ void updateProduct(int productId, Map<int, int> variantId, int quantity) {
+ final copy = Map.of(state.productsWithQuantity);
+ copy[productId] = ProductInCart(productId, variantId, quantity);
+ safeEmit(state.copyWith(productsWithQuantity: copy));
}
+ ProductInCart? getProduct(int productId) => state.productsWithQuantity[productId];
+
+ int get totalItems {
+ if (state.productsWithQuantity.isEmpty) return 0;
+ return state.productsWithQuantity.values
+ .map((p) => p.quantity)
+ .reduce((prev, curr) => prev + curr);
+ }
- void removeProduct(Product product) {
- final copy = List.of(state.productsInCart);
- copy.remove(product);
- safeEmit(state.copyWith(productsInCart: copy));
+ double get totalPrice {
+ double price = 0;
+ for (final product in products) {
+ final quantity = getProduct(product.id)?.quantity ?? 0;
+ price += product.price * quantity;
+ }
+ return price;
}
- int getQuantityForProduct(int productId) {
- return state.productsInCart.map((p) => p.id == productId).length;
+ Iterable<Product> get products sync* {
+ for (final productId in state.productsWithQuantity.keys) {
+ final dataState = _conciergeCubit.getProductState(productId);
+ if (dataState is Success<Product>) yield dataState.data;
+ }
}
}
@@ -31,8 +49,16 @@ abstract class CartState with _$CartState {
const factory CartState({
@Default(false) bool isLoading,
@Default(AppError.none) AppError error,
- @Default([]) List<Product> productsInCart,
+ @Default({}) Map<int, ProductInCart> productsWithQuantity,
}) = _CartState;
const CartState._();
}
+
+class ProductInCart {
+ final int productId;
+ final Map<int, int> variant;
+ final int quantity;
+
+ ProductInCart(this.productId, this.variant, this.quantity);
+}
diff --git a/concierge/lib/presentation/navigation/transitions/slide_up_transition.dart b/concierge/lib/presentation/navigation/transitions/slide_up_transition.dart
index 1b25403f..35912e0e 100644
--- a/concierge/lib/presentation/navigation/transitions/slide_up_transition.dart
+++ b/concierge/lib/presentation/navigation/transitions/slide_up_transition.dart
@@ -7,7 +7,7 @@ Page slideUpTransition({
}) {
return CustomTransitionPage(
key: state.pageKey,
- transitionDuration: Duration(milliseconds: 300),
+ transitionDuration: Duration(milliseconds: 200),
child: child,
transitionsBuilder: (context, animation, secondaryAnimation, child) {
return SlideTransition(
diff --git a/concierge/lib/presentation/screens/confirm_order/confirm_order_route.dart b/concierge/lib/presentation/screens/confirm_order/confirm_order_route.dart
index ead528d9..d92a15d1 100644
--- a/concierge/lib/presentation/screens/confirm_order/confirm_order_route.dart
+++ b/concierge/lib/presentation/screens/confirm_order/confirm_order_route.dart
@@ -1,8 +1,8 @@
+import 'package:concierge/presentation/navigation/transitions/slide_in_transition.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:concierge/presentation/navigation/app_routes.dart';
import 'package:go_router/go_router.dart';
-import 'package:concierge/presentation/navigation/transitions/slide_up_transition.dart';
import 'package:concierge/presentation/screens/confirm_order/bloc/confirm_order_cubit.dart';
import 'package:concierge/presentation/screens/confirm_order/confirm_order_screen.dart';
@@ -12,7 +12,7 @@ part '../../../_generated/presentation/screens/confirm_order/confirm_order_route
class ConfirmOrderRoute extends GoRouteData with $ConfirmOrderRoute {
@override
Page<void> buildPage(BuildContext context, GoRouterState state) {
- return slideUpTransition(
+ return slideInTransition(
state: state,
child: BlocProvider(
create: (context) => ConfirmOrderCubit(),
diff --git a/concierge/lib/presentation/screens/confirm_order/confirm_order_screen.dart b/concierge/lib/presentation/screens/confirm_order/confirm_order_screen.dart
index 8841ba6d..71b12136 100644
--- a/concierge/lib/presentation/screens/confirm_order/confirm_order_screen.dart
+++ b/concierge/lib/presentation/screens/confirm_order/confirm_order_screen.dart
@@ -1,3 +1,4 @@
+import 'package:concierge/presentation/screens/payment/payment_route.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:concierge/presentation/screens/confirm_order/bloc/confirm_order_cubit.dart';
@@ -14,11 +15,9 @@ class ConfirmOrderScreen extends StatelessWidget {
return MultiBlocListener(
listeners: [
BlocListener<ConfirmOrderCubit, ConfirmOrderState>(
- listenWhen: (prev, curr) =>
- prev.isLoading && curr.error.isError,
- listener: (context, state) {
- },
- )
+ listenWhen: (prev, curr) => prev.isLoading && curr.error.isError,
+ listener: (context, state) {},
+ ),
],
child: Scaffold(
appBar: AppBar(),
@@ -26,6 +25,12 @@ class ConfirmOrderScreen extends StatelessWidget {
child: Column(
children: [
Text("ConfirmOrder"),
+ TextButton(
+ onPressed: () {
+ PaymentRoute().push(context);
+ },
+ child: Text("Gå til betaling"),
+ ),
],
),
),
@@ -33,5 +38,5 @@ class ConfirmOrderScreen extends StatelessWidget {
);
},
);
- }
-}
\ No newline at end of file
+ }
+}
diff --git a/concierge/lib/presentation/screens/hotel_overview_page/hotel_overview_page_screen.dart b/concierge/lib/presentation/screens/hotel_overview_page/hotel_overview_page_screen.dart
index 111d2335..8046c91e 100644
--- a/concierge/lib/presentation/screens/hotel_overview_page/hotel_overview_page_screen.dart
+++ b/concierge/lib/presentation/screens/hotel_overview_page/hotel_overview_page_screen.dart
@@ -2,6 +2,7 @@ import 'package:concierge/data/remote/models/area_category.dart';
import 'package:concierge/data/remote/models/area_details.dart';
import 'package:concierge/data/remote/models/area_sub_category.dart';
import 'package:concierge/domain/models/data_state.dart';
+import 'package:concierge/presentation/screens/hotel_overview_page/widgets/cart_app_bar.dart';
import 'package:concierge/presentation/screens/hotel_overview_page/widgets/product_list_tile.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@@ -27,6 +28,7 @@ class HotelOverviewPageScreen extends StatelessWidget {
),
],
child: Scaffold(
+ bottomSheet: CartAppBar(),
appBar: HotelOverviewAppBar(
areas: state.property?.areas ?? const [],
selectedAreaId: state.selectedAreaId,
@@ -44,6 +46,9 @@ class HotelOverviewPageScreen extends StatelessWidget {
return Center(child: Text((areaState).error.toString()));
}
final areaDetails = (areaState as Success<AreaDetails>).data;
+ if (areaDetails.categories.isEmpty) {
+ return Center(child: Text("Der er ingen varer på til dette område"));
+ }
final widgets = buildCategories(context, areaDetails.categories).toList();
return ListView.builder(
itemCount: widgets.length,
@@ -64,19 +69,21 @@ class HotelOverviewPageScreen extends StatelessWidget {
BuildContext context,
List<AreaCategory> categories,
) sync* {
+ yield Gap(24);
for (final category in categories) {
yield Padding(
- padding: const EdgeInsets.all(16.0),
+ padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Text(
category.name,
style: TextStyle(fontSize: 24),
),
);
- yield Gap(16);
+ yield Gap(12);
yield* buildSubCategories(
context,
category.subCategories,
);
+ yield Gap(100);
}
}
@@ -87,12 +94,13 @@ class HotelOverviewPageScreen extends StatelessWidget {
for (final category in subCategories) {
final products = category.products;
yield Padding(
- padding: const EdgeInsets.all(16.0),
+ padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Text(
category.name,
style: TextStyle(fontSize: 16),
),
);
+ yield Gap(8);
yield SizedBox(
height: 320,
child: ListView.builder(
@@ -108,6 +116,7 @@ class HotelOverviewPageScreen extends StatelessWidget {
},
),
);
+ yield Gap(24);
}
}
}
diff --git a/concierge/lib/presentation/screens/hotel_overview_page/widgets/cart_app_bar.dart b/concierge/lib/presentation/screens/hotel_overview_page/widgets/cart_app_bar.dart
new file mode 100644
index 00000000..d2a5bce2
--- /dev/null
+++ b/concierge/lib/presentation/screens/hotel_overview_page/widgets/cart_app_bar.dart
@@ -0,0 +1,69 @@
+import 'package:concierge/presentation/app/cart_cubit.dart';
+import 'package:concierge/presentation/screens/review_order/review_order_route.dart';
+import 'package:concierge/presentation/theme/app_colors.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:gap/gap.dart';
+
+class CartAppBar extends StatelessWidget {
+ const CartAppBar({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ final cartCubit = context.watch<CartCubit>();
+ return Container(
+ decoration: BoxDecoration(
+ color: AppColors.sandColor,
+ borderRadius: BorderRadius.vertical(
+ top: Radius.circular(24),
+ ),
+ ),
+ height: 100,
+ child: Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 16.0),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Column(
+ mainAxisSize: MainAxisSize.min,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ "Min Bestilling",
+ style: TextStyle(color: Colors.white),
+ ),
+ Text(
+ "${cartCubit.totalItems} varer",
+ style: TextStyle(color: Colors.white, fontSize: 12),
+ ),
+ ],
+ ),
+ Row(
+ children: [
+ Text(
+ "${cartCubit.totalPrice.toString()} DKK",
+ style: TextStyle(color: Colors.white),
+ ),
+ Gap(8),
+ SizedBox(
+ height: 24,
+ child: VerticalDivider(
+ color: Colors.white,
+ width: 1,
+ ),
+ ),
+ Gap(8),
+ IconButton(
+ onPressed: () {
+ ReviewOrderRoute().push(context);
+ },
+ icon: Icon(Icons.keyboard_arrow_up),
+ ),
+ ],
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}
diff --git a/concierge/lib/presentation/screens/hotel_overview_page/widgets/product_list_tile.dart b/concierge/lib/presentation/screens/hotel_overview_page/widgets/product_list_tile.dart
index 00db1887..c13851a5 100644
--- a/concierge/lib/presentation/screens/hotel_overview_page/widgets/product_list_tile.dart
+++ b/concierge/lib/presentation/screens/hotel_overview_page/widgets/product_list_tile.dart
@@ -29,7 +29,7 @@ class ProductListTile extends StatelessWidget {
child: Builder(
builder: (context) {
if (dataState is Loading) {
- return Center(child: Text("LOADING"));
+ return Center(child: CircularProgressIndicator());
}
if (dataState is Failure) {
return Center(child: Text(dataState.error.toString()));
@@ -40,43 +40,57 @@ class ProductListTile extends StatelessWidget {
onTap: () {
ProductDetailsRoute(product.id).push(context);
},
- child: ClipRect(
- clipBehavior: Clip.antiAlias,
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- if (product.images.isNotEmpty)
- Image.network(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ if (product.images.isNotEmpty)
+ Container(
+ clipBehavior: Clip.antiAlias,
+ decoration: BoxDecoration(
+ borderRadius: BorderRadius.vertical(top: Radius.circular(15)),
+ ),
+ child: Image.network(
product.images.first.url,
fit: BoxFit.fitWidth,
height: 180,
width: double.infinity,
- )
- else
- SizedBox(height: 180),
- PaddedColumn(
- padding: EdgeInsets.all(16),
- children: [
- Row(
- children: [
- Icon(Icons.watch_later_outlined),
- Text(product.estimatedDeliveryTime),
- ],
- ),
- Gap(16),
- Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
- children: [
- Text(product.title),
- Text(product.price.toString()),
- ],
- ),
- Gap(8),
- Text(product.subTitle),
- ],
- ),
- ],
- ),
+ ),
+ )
+ else
+ SizedBox(height: 180),
+ PaddedColumn(
+ padding: EdgeInsets.all(16),
+ children: [
+ Row(
+ children: [
+ Icon(Icons.watch_later_outlined),
+ Gap(4),
+ Text(product.estimatedDeliveryTime),
+ ],
+ ),
+ Gap(16),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Expanded(
+ child: Text(
+ product.title,
+ overflow: TextOverflow.ellipsis,
+ ),
+ ),
+ Gap(8),
+ Text(product.price.toString()),
+ ],
+ ),
+ Gap(8),
+ Text(
+ product.subTitle,
+ maxLines: 1,
+ overflow: TextOverflow.ellipsis,
+ ),
+ ],
+ ),
+ ],
),
);
},
diff --git a/concierge/lib/presentation/screens/payment/payment_route.dart b/concierge/lib/presentation/screens/payment/payment_route.dart
index 4df7a2e8..38597ade 100644
--- a/concierge/lib/presentation/screens/payment/payment_route.dart
+++ b/concierge/lib/presentation/screens/payment/payment_route.dart
@@ -6,13 +6,15 @@ import 'package:concierge/presentation/navigation/transitions/slide_up_transitio
import 'package:concierge/presentation/screens/payment/bloc/payment_cubit.dart';
import 'package:concierge/presentation/screens/payment/payment_screen.dart';
+import '../../navigation/transitions/slide_in_transition.dart';
+
part '../../../_generated/presentation/screens/payment/payment_route.g.dart';
@TypedGoRoute<PaymentRoute>(path: AppRoutes.payment)
class PaymentRoute extends GoRouteData with $PaymentRoute {
@override
Page<void> buildPage(BuildContext context, GoRouterState state) {
- return slideUpTransition(
+ return slideInTransition(
state: state,
child: BlocProvider(
create: (context) => PaymentCubit(),
diff --git a/concierge/lib/presentation/screens/payment/payment_screen.dart b/concierge/lib/presentation/screens/payment/payment_screen.dart
index cec212bd..73c0676c 100644
--- a/concierge/lib/presentation/screens/payment/payment_screen.dart
+++ b/concierge/lib/presentation/screens/payment/payment_screen.dart
@@ -1,3 +1,4 @@
+import 'package:concierge/presentation/screens/receipt/receipt_route.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:concierge/presentation/screens/payment/bloc/payment_cubit.dart';
@@ -26,6 +27,13 @@ class PaymentScreen extends StatelessWidget {
child: Column(
children: [
Text("Payment"),
+ Text("SUCCESS!"),
+ TextButton(
+ onPressed: () {
+ ReceiptRoute().push(context);
+ },
+ child: Text("Gå til kvittering"),
+ )
],
),
),
diff --git a/concierge/lib/presentation/screens/product_details/bloc/product_details_cubit.dart b/concierge/lib/presentation/screens/product_details/bloc/product_details_cubit.dart
index ed90cf96..58b37da0 100644
--- a/concierge/lib/presentation/screens/product_details/bloc/product_details_cubit.dart
+++ b/concierge/lib/presentation/screens/product_details/bloc/product_details_cubit.dart
@@ -3,47 +3,83 @@ import 'dart:async';
import 'package:concierge/data/remote/models/product.dart';
import 'package:concierge/domain/models/app_error.dart';
import 'package:concierge/domain/models/data_state.dart';
+import 'package:concierge/presentation/app/cart_cubit.dart';
import 'package:concierge/presentation/app/concierge_cubit.dart';
import 'package:concierge/presentation/base/base_cubit.dart';
import 'package:concierge/presentation/screens/product_details/bloc/product_details_state.dart';
class ProductDetailsCubit extends BaseCubit<ProductDetailsState> {
final ConciergeCubit _conciergeCubit;
+ final CartCubit _cartCubit;
ProductDetailsCubit(
- this._conciergeCubit, {
+ this._conciergeCubit,
+ this._cartCubit, {
required this.productId,
- }) : super(const ProductDetailsState()) {
+ }) : super(ProductDetailsState()) {
init();
}
late final StreamSubscription<ConciergeState> conciergeStateStream;
final int productId;
+ void handleProductDataState(DataState dataState) {
+ switch (dataState) {
+ case Initial _:
+ case Loading _:
+ safeEmit(state.copyWith(isLoading: true, error: AppError.none));
+ break;
+ case Success _:
+ safeEmit(
+ state.copyWith(isLoading: false, product: dataState.data as Product),
+ );
+ break;
+ case Failure _:
+ safeEmit(
+ state.copyWith(
+ isLoading: false,
+ error: AppError.unknown((dataState).error.toString()),
+ ),
+ );
+ }
+ }
+
Future<void> init() async {
+ final productInCart = _cartCubit.getProduct(productId);
+ if (productInCart != null) {
+ safeEmit(
+ state.copyWith(
+ quantity: productInCart.quantity,
+ selectedVariant: productInCart.variant,
+ ),
+ );
+ }
+ handleProductDataState(_conciergeCubit.getProductState(productId));
conciergeStateStream = _conciergeCubit.stream.listen((conciergeState) {
final dataState = _conciergeCubit.getProductState(productId);
- switch (dataState) {
- case Initial _:
- case Loading _:
- safeEmit(state.copyWith(isLoading: true, error: AppError.none));
- break;
- case Success _:
- safeEmit(
- state.copyWith(isLoading: false, product: dataState.data as Product),
- );
- break;
- case Failure _:
- safeEmit(
- state.copyWith(
- isLoading: false,
- error: AppError.unknown((dataState).error.toString()),
- ),
- );
- }
+ handleProductDataState(dataState);
});
}
+ void increaseQuantity() {
+ safeEmit(state.copyWith(quantity: state.quantity + 1));
+ }
+
+ void decreaseQuantity() {
+ final q = (state.quantity - 1).clamp(0, state.quantity);
+ safeEmit(state.copyWith(quantity: q));
+ }
+
+ void submitToCart() {
+ _cartCubit.updateProduct(productId, state.selectedVariant, state.quantity);
+ }
+
+ void onVariantSelected(int optionId, int answerId) {
+ final copy = Map.of(state.selectedVariant);
+ copy[optionId] = answerId;
+ safeEmit(state.copyWith(selectedVariant: copy));
+ }
+
@override
Future<void> close() async {
await conciergeStateStream.cancel();
diff --git a/concierge/lib/presentation/screens/product_details/bloc/product_details_state.dart b/concierge/lib/presentation/screens/product_details/bloc/product_details_state.dart
index a604f70e..1eb9adb0 100644
--- a/concierge/lib/presentation/screens/product_details/bloc/product_details_state.dart
+++ b/concierge/lib/presentation/screens/product_details/bloc/product_details_state.dart
@@ -9,10 +9,12 @@ abstract class ProductDetailsState with _$ProductDetailsState {
const factory ProductDetailsState({
@Default(false) bool isLoading,
@Default(AppError.none) AppError error,
+ @Default(0) int quantity,
+ @Default({}) Map<int, int> selectedVariant,
Product? product,
}) = _ProductDetailsState;
const ProductDetailsState._();
Product get requireProduct => product!;
-}
\ No newline at end of file
+}
diff --git a/concierge/lib/presentation/screens/product_details/product_details_route.dart b/concierge/lib/presentation/screens/product_details/product_details_route.dart
index 121552a0..63591a3e 100644
--- a/concierge/lib/presentation/screens/product_details/product_details_route.dart
+++ b/concierge/lib/presentation/screens/product_details/product_details_route.dart
@@ -19,7 +19,11 @@ class ProductDetailsRoute extends GoRouteData with $ProductDetailsRoute {
return slideInTransition(
state: state,
child: BlocProvider(
- create: (context) => ProductDetailsCubit(context.read(), productId: id),
+ create: (context) => ProductDetailsCubit(
+ context.read(),
+ context.read(),
+ productId: id,
+ ),
child: ProductDetailsScreen(),
),
);
diff --git a/concierge/lib/presentation/screens/product_details/product_details_screen.dart b/concierge/lib/presentation/screens/product_details/product_details_screen.dart
index 733b7459..d7e9cfb5 100644
--- a/concierge/lib/presentation/screens/product_details/product_details_screen.dart
+++ b/concierge/lib/presentation/screens/product_details/product_details_screen.dart
@@ -1,16 +1,18 @@
-import 'package:concierge/presentation/app/cart_cubit.dart';
-import 'package:concierge/presentation/screens/hotel_overview_page/widgets/product_list_tile.dart';
+import 'package:concierge/data/remote/models/product.dart';
+import 'package:concierge/presentation/screens/product_details/widgets/product_details_app_bar.dart';
+import 'package:concierge/presentation/widgets/padded_column.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:concierge/presentation/screens/product_details/bloc/product_details_cubit.dart';
import 'package:concierge/presentation/screens/product_details/bloc/product_details_state.dart';
+import 'package:flutter_html/flutter_html.dart';
+import 'package:gap/gap.dart';
class ProductDetailsScreen extends StatelessWidget {
const ProductDetailsScreen({super.key});
@override
Widget build(BuildContext context) {
- final cartCubit = context.watch<CartCubit>();
final cubit = context.read<ProductDetailsCubit>();
return BlocBuilder<ProductDetailsCubit, ProductDetailsState>(
builder: (context, state) {
@@ -22,46 +24,154 @@ class ProductDetailsScreen extends StatelessWidget {
),
],
child: Scaffold(
- appBar: AppBar(
- backgroundColor: Colors.white,
- ),
+ bottomNavigationBar: SafeArea(child: ProductDetailsAppBar()),
backgroundColor: Colors.white,
- body: Builder(
- builder: (context) {
- if (state.isLoading) {
- return Center(child: CircularProgressIndicator());
- }
- if (state.error.isError) {
- return Center(child: Text(state.error.toString()));
- }
- return Column(
- children: [
- ProductListTile(productId: cubit.productId),
- Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ body: Stack(
+ alignment: Alignment.topCenter,
+ children: [
+ Builder(
+ builder: (context) {
+ if (state.isLoading) {
+ return Center(child: CircularProgressIndicator());
+ }
+ if (state.error.isError) {
+ return Center(child: Text(state.error.toString()));
+ }
+ return SingleChildScrollView(
+ child: Column(
+ children: [
+ Image.network(
+ state.requireProduct.images.first.url,
+ fit: BoxFit.fitHeight,
+ height: 400,
+ width: double.infinity,
+ ),
+ PaddedColumn(
+ padding: EdgeInsets.symmetric(horizontal: 16),
+ children: [
+ const Gap(8),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Row(
+ children: [
+ const Icon(Icons.watch_later_outlined),
+ const Gap(5),
+ Text(state.requireProduct.estimatedDeliveryTime),
+ ],
+ ),
+ IconButton(
+ onPressed: () {},
+ icon: const Icon(Icons.info_outline),
+ ),
+ ],
+ ),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Text(state.requireProduct.title),
+ Text("${state.requireProduct.price} kr."),
+ ],
+ ),
+ const Gap(8),
+ Text(state.requireProduct.subTitle),
+ const Gap(16),
+ Divider(color: Colors.grey),
+ const Gap(16),
+ Html(data: state.requireProduct.body),
+ Gap(48),
+ ...buildVariants(context, state.requireProduct),
+ Gap(100),
+ ],
+ ),
+ ],
+ ),
+ );
+ },
+ ),
+ Container(
+ height: 100,
+ decoration: BoxDecoration(
+ color: Colors.white,
+ borderRadius: BorderRadius.vertical(bottom: Radius.circular(24)),
+ ),
+ child: Padding(
+ padding: const EdgeInsets.all(8.0),
+ child: Row(
+ crossAxisAlignment: CrossAxisAlignment.end,
children: [
- IconButton(
- onPressed: () {
- cartCubit.removeProduct(state.requireProduct);
- },
- icon: Icon(Icons.remove),
- ),
- Text(cartCubit.getQuantityForProduct(cubit.productId).toString()),
- IconButton(
- onPressed: () {
- cartCubit.addToCart(state.requireProduct);
- },
- icon: Icon(Icons.add),
- ),
+ Spacer(),
+ CloseButton(color: Colors.black),
],
),
- ],
- );
- },
+ ),
+ ),
+ ],
),
),
);
},
);
}
+
+ Iterable<Widget> buildVariants(BuildContext context, Product product) sync* {
+ final cubit = context.read<ProductDetailsCubit>();
+ for (final option in product.options) {
+ yield Text(option.name, style: TextStyle(fontSize: 20));
+ yield Divider(color: Colors.grey);
+ for (final answer in option.answers) {
+ yield InkWell(
+ onTap: () {
+ cubit.onVariantSelected(option.id, answer.id);
+ },
+ child: Padding(
+ padding: const EdgeInsets.symmetric(vertical: 12.0),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Text(answer.name),
+ Container(
+ height: 20,
+ decoration: BoxDecoration(
+ shape: BoxShape.circle,
+ color: Colors.black,
+ border: Border.all(color: Colors.grey),
+ ),
+ child: buildAnswerCheckmark(answer.id == cubit.state.selectedVariant[option.id]),
+ ),
+ ],
+ ),
+ ),
+ );
+ yield Divider(color: Colors.grey);
+ }
+ }
+ }
+
+ Widget buildAnswerCheckmark(bool isSelected) {
+ if (isSelected) {
+ return Container(
+ height: 20,
+ width: 20,
+ decoration: BoxDecoration(
+ shape: BoxShape.circle,
+ color: Colors.black,
+ ),
+ child: Icon(
+ Icons.check,
+ size: 12,
+ color: Colors.white,
+ ),
+ );
+ }
+ return Container(
+ height: 20,
+ width: 20,
+ decoration: BoxDecoration(
+ shape: BoxShape.circle,
+ color: Colors.white,
+ border: Border.all(color: Colors.grey, width: 1),
+ ),
+ );
+ }
}
diff --git a/concierge/lib/presentation/screens/product_details/widgets/product_details_app_bar.dart b/concierge/lib/presentation/screens/product_details/widgets/product_details_app_bar.dart
new file mode 100644
index 00000000..7cca6dc6
--- /dev/null
+++ b/concierge/lib/presentation/screens/product_details/widgets/product_details_app_bar.dart
@@ -0,0 +1,69 @@
+import 'package:concierge/presentation/app/cart_cubit.dart';
+import 'package:concierge/presentation/screens/product_details/bloc/product_details_cubit.dart';
+import 'package:concierge/presentation/theme/app_colors.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:gap/gap.dart';
+import 'package:go_router/go_router.dart';
+
+class ProductDetailsAppBar extends StatelessWidget {
+ const ProductDetailsAppBar({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ final cartCubit = context.watch<CartCubit>();
+ final productDetailsCubit = context.read<ProductDetailsCubit>();
+ final state = productDetailsCubit.state;
+ return Padding(
+ padding: const EdgeInsets.all(16.0),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Container(
+ decoration: BoxDecoration(shape: BoxShape.circle, color: Colors.black),
+ child: IconButton(
+ color: Colors.white,
+ onPressed: () {
+ productDetailsCubit.decreaseQuantity();
+ },
+ icon: Icon(Icons.remove),
+ ),
+ ),
+ const Gap(10),
+ Text(state.quantity.toString(), style: TextStyle(fontSize: 18),),
+ const Gap(10),
+ Container(
+ decoration: BoxDecoration(shape: BoxShape.circle, color: Colors.black),
+ child: IconButton(
+ color: Colors.white,
+ onPressed: () {
+ productDetailsCubit.increaseQuantity();
+ },
+ icon: Icon(Icons.add),
+ ),
+ ),
+ const Gap(20),
+ Expanded(
+ child: TextButton(
+ style: ButtonStyle(
+ shape: WidgetStatePropertyAll(
+ RoundedRectangleBorder(borderRadius: BorderRadius.circular(45)),
+ ),
+ minimumSize: WidgetStatePropertyAll(Size(0, 50)),
+ backgroundColor: WidgetStatePropertyAll(AppColors.sandColor),
+ ),
+ onPressed: () {
+ productDetailsCubit.submitToCart();
+ context.pop();
+ },
+ child: Text(
+ "Add to order",
+ style: TextStyle(color: Colors.white),
+ ),
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/concierge/lib/presentation/screens/provide_location/provide_location_route.dart b/concierge/lib/presentation/screens/provide_location/provide_location_route.dart
index 28fe2a72..e9fb00c7 100644
--- a/concierge/lib/presentation/screens/provide_location/provide_location_route.dart
+++ b/concierge/lib/presentation/screens/provide_location/provide_location_route.dart
@@ -1,8 +1,8 @@
+import 'package:concierge/presentation/navigation/transitions/slide_in_transition.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:concierge/presentation/navigation/app_routes.dart';
import 'package:go_router/go_router.dart';
-import 'package:concierge/presentation/navigation/transitions/slide_up_transition.dart';
import 'package:concierge/presentation/screens/provide_location/bloc/provide_location_cubit.dart';
import 'package:concierge/presentation/screens/provide_location/provide_location_screen.dart';
@@ -12,7 +12,7 @@ part '../../../_generated/presentation/screens/provide_location/provide_location
class ProvideLocationRoute extends GoRouteData with $ProvideLocationRoute {
@override
Page<void> buildPage(BuildContext context, GoRouterState state) {
- return slideUpTransition(
+ return slideInTransition(
state: state,
child: BlocProvider(
create: (context) => ProvideLocationCubit(),
diff --git a/concierge/lib/presentation/screens/provide_location/provide_location_screen.dart b/concierge/lib/presentation/screens/provide_location/provide_location_screen.dart
index f056b288..55f85e56 100644
--- a/concierge/lib/presentation/screens/provide_location/provide_location_screen.dart
+++ b/concierge/lib/presentation/screens/provide_location/provide_location_screen.dart
@@ -1,3 +1,4 @@
+import 'package:concierge/presentation/screens/confirm_order/confirm_order_route.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:concierge/presentation/screens/provide_location/bloc/provide_location_cubit.dart';
@@ -14,11 +15,9 @@ class ProvideLocationScreen extends StatelessWidget {
return MultiBlocListener(
listeners: [
BlocListener<ProvideLocationCubit, ProvideLocationState>(
- listenWhen: (prev, curr) =>
- prev.isLoading && curr.error.isError,
- listener: (context, state) {
- },
- )
+ listenWhen: (prev, curr) => prev.isLoading && curr.error.isError,
+ listener: (context, state) {},
+ ),
],
child: Scaffold(
appBar: AppBar(),
@@ -26,6 +25,12 @@ class ProvideLocationScreen extends StatelessWidget {
child: Column(
children: [
Text("ProvideLocation"),
+ TextButton(
+ onPressed: () {
+ ConfirmOrderRoute().push(context);
+ },
+ child: Text("Bekræft ordre"),
+ ),
],
),
),
@@ -33,5 +38,5 @@ class ProvideLocationScreen extends StatelessWidget {
);
},
);
- }
-}
\ No newline at end of file
+ }
+}
diff --git a/concierge/lib/presentation/screens/receipt/receipt_route.dart b/concierge/lib/presentation/screens/receipt/receipt_route.dart
index 4409d222..de53ffd1 100644
--- a/concierge/lib/presentation/screens/receipt/receipt_route.dart
+++ b/concierge/lib/presentation/screens/receipt/receipt_route.dart
@@ -6,13 +6,15 @@ import 'package:concierge/presentation/navigation/transitions/slide_up_transitio
import 'package:concierge/presentation/screens/receipt/bloc/receipt_cubit.dart';
import 'package:concierge/presentation/screens/receipt/receipt_screen.dart';
+import '../../navigation/transitions/slide_in_transition.dart';
+
part '../../../_generated/presentation/screens/receipt/receipt_route.g.dart';
@TypedGoRoute<ReceiptRoute>(path: AppRoutes.receipt)
class ReceiptRoute extends GoRouteData with $ReceiptRoute {
@override
Page<void> buildPage(BuildContext context, GoRouterState state) {
- return slideUpTransition(
+ return slideInTransition(
state: state,
child: BlocProvider(
create: (context) => ReceiptCubit(),
diff --git a/concierge/lib/presentation/screens/receipt/receipt_screen.dart b/concierge/lib/presentation/screens/receipt/receipt_screen.dart
index 46fc6d70..ad0a10b5 100644
--- a/concierge/lib/presentation/screens/receipt/receipt_screen.dart
+++ b/concierge/lib/presentation/screens/receipt/receipt_screen.dart
@@ -1,3 +1,4 @@
+import 'package:concierge/presentation/screens/hotel_overview_page/hotel_overview_page_route.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:concierge/presentation/screens/receipt/bloc/receipt_cubit.dart';
@@ -26,6 +27,12 @@ class ReceiptScreen extends StatelessWidget {
child: Column(
children: [
Text("Receipt"),
+ TextButton(
+ onPressed: () {
+
+ },
+ child: Text("OK"),
+ )
],
),
),
diff --git a/concierge/lib/presentation/screens/review_order/review_order_screen.dart b/concierge/lib/presentation/screens/review_order/review_order_screen.dart
index edc8083b..6521a64e 100644
--- a/concierge/lib/presentation/screens/review_order/review_order_screen.dart
+++ b/concierge/lib/presentation/screens/review_order/review_order_screen.dart
@@ -1,3 +1,4 @@
+import 'package:concierge/presentation/screens/provide_location/provide_location_route.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:concierge/presentation/screens/review_order/bloc/review_order_cubit.dart';
@@ -26,6 +27,9 @@ class ReviewOrderScreen extends StatelessWidget {
child: Column(
children: [
Text("ReviewOrder"),
+ TextButton(onPressed: () {
+ ProvideLocationRoute().push(context);
+ }, child: Text("Angiv lokation"))
],
),
),
diff --git a/concierge/pubspec.yaml b/concierge/pubspec.yaml
index cddfd12d..fac833f6 100644
--- a/concierge/pubspec.yaml
+++ b/concierge/pubspec.yaml
@@ -29,6 +29,7 @@ dependencies:
flutter_svg: ^2.2.1
flutter_secure_storage: ^9.2.4
fpdart: ^1.2.0
+ flutter_html: ^3.0.0
dev_dependencies:
flutter_test: