6177214e-ce7c-49e3-99de-ff9721b26f63 — Commit 0cf3e649

AuthorMikkel Thygesen<mikkelet@gmail.com>
Date2026-02-27 11:41:34 +0100
2922: refined review_order_screen.dart

Changed files

.../bloc/product_details_state.freezed.dart        |  2 +-
 .../product_details/product_details_route.g.dart   | 34 +----------
 concierge/lib/presentation/app/cart_cubit.dart     | 12 +++-
 .../bloc/product_details_cubit.dart                |  4 +-
 .../bloc/product_details_state.dart                |  2 +-
 .../product_details/product_details_route.dart     |  4 +-
 .../product_details/product_details_screen.dart    | 19 +-----
 .../widgets/product_details_app_bar.dart           |  1 -
 .../screens/review_order/review_order_screen.dart  | 64 +++++++++++++++-----
 .../widget/delivery_method_picker.dart             | 68 ++++++++++++++++------
 .../widget/product_in_cart_list_tile.dart          | 66 +++++++++++++--------
 .../review_order/widget/review_order_app_bar.dart  | 15 +++--
 .../lib/presentation/widgets/bevelled_app_bar.dart | 27 +++++++++
 13 files changed, 194 insertions(+), 124 deletions(-)

Diff

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 19ed9185..7a7ac792 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
@@ -210,7 +210,7 @@ return $default(_that.isLoading,_that.error,_that.quantity,_that.selectedVariant
class _ProductDetailsState extends ProductDetailsState {
- const _ProductDetailsState({this.isLoading = false, this.error = AppError.none, this.quantity = 0, final Map<int, int> selectedVariant = const {}, this.product}): _selectedVariant = selectedVariant,super._();
+ const _ProductDetailsState({this.isLoading = false, this.error = AppError.none, this.quantity = 1, final Map<int, int> selectedVariant = const {}, this.product}): _selectedVariant = selectedVariant,super._();
@override@JsonKey() final bool isLoading;
diff --git a/concierge/lib/_generated/presentation/screens/product_details/product_details_route.g.dart b/concierge/lib/_generated/presentation/screens/product_details/product_details_route.g.dart
index e4941918..0b14509e 100644
--- a/concierge/lib/_generated/presentation/screens/product_details/product_details_route.g.dart
+++ b/concierge/lib/_generated/presentation/screens/product_details/product_details_route.g.dart
@@ -15,25 +15,13 @@ RouteBase get $productDetailsRoute => GoRouteData.$route(
mixin $ProductDetailsRoute on GoRouteData {
static ProductDetailsRoute _fromState(GoRouterState state) =>
- ProductDetailsRoute(
- int.parse(state.pathParameters['id']!),
- isEdit:
- _$convertMapValue(
- 'is-edit',
- state.uri.queryParameters,
- _$boolConverter,
- ) ??
- false,
- );
+ ProductDetailsRoute(int.parse(state.pathParameters['id']!));
ProductDetailsRoute get _self => this as ProductDetailsRoute;
@override
String get location => GoRouteData.$location(
'/concierge/products/${Uri.encodeComponent(_self.id.toString())}',
- queryParams: {
- if (_self.isEdit != false) 'is-edit': _self.isEdit.toString(),
- },
);
@override
@@ -49,23 +37,3 @@ mixin $ProductDetailsRoute on GoRouteData {
@override
void replace(BuildContext context) => context.replace(location);
}
-
-T? _$convertMapValue<T>(
- String key,
- Map<String, String> map,
- T? Function(String) converter,
-) {
- final value = map[key];
- return value == null ? null : converter(value);
-}
-
-bool _$boolConverter(String value) {
- switch (value) {
- case 'true':
- return true;
- case 'false':
- return false;
- default:
- throw UnsupportedError('Cannot convert "$value" into a bool.');
- }
-}
diff --git a/concierge/lib/presentation/app/cart_cubit.dart b/concierge/lib/presentation/app/cart_cubit.dart
index 5e72e5f1..1df15bc0 100644
--- a/concierge/lib/presentation/app/cart_cubit.dart
+++ b/concierge/lib/presentation/app/cart_cubit.dart
@@ -4,6 +4,7 @@ import 'package:concierge/domain/models/delivery_method.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:flutter/cupertino.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import '../../domain/models/product_in_cart.dart';
@@ -15,13 +16,20 @@ class CartCubit extends BaseCubit<CartState> {
CartCubit(this._conciergeCubit) : super(const CartState());
+ final orderCommentController = TextEditingController();
+
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];
+ ProductInCart? getProduct(int productId) {
+ final product = state.productsWithQuantity[productId];
+ if (product == null) return null;
+ if (product.quantity == 0) return null;
+ return product;
+ }
int getQuantity(int productId) => state.productsWithQuantity[productId]?.quantity ?? 0;
@@ -52,7 +60,7 @@ class CartCubit extends BaseCubit<CartState> {
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;
+ if (dataState is Success<Product> && getQuantity(productId) > 0) yield dataState.data;
}
}
}
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 98524ce3..c6a6802c 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
@@ -16,14 +16,14 @@ class ProductDetailsCubit extends BaseCubit<ProductDetailsState> {
this._conciergeCubit,
this._cartCubit, {
required this.productId,
- required this.isEdit,
}) : super(ProductDetailsState()) {
init();
}
late final StreamSubscription<ConciergeState> conciergeStateStream;
final int productId;
- final bool isEdit;
+
+ bool get isEdit => _cartCubit.getProduct(productId) != null;
void handleProductDataState(DataState dataState) {
switch (dataState) {
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 1eb9adb0..89b5cc58 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,7 +9,7 @@ abstract class ProductDetailsState with _$ProductDetailsState {
const factory ProductDetailsState({
@Default(false) bool isLoading,
@Default(AppError.none) AppError error,
- @Default(0) int quantity,
+ @Default(1) int quantity,
@Default({}) Map<int, int> selectedVariant,
Product? product,
}) = _ProductDetailsState;
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 be60c1f9..63591a3e 100644
--- a/concierge/lib/presentation/screens/product_details/product_details_route.dart
+++ b/concierge/lib/presentation/screens/product_details/product_details_route.dart
@@ -11,9 +11,8 @@ part '../../../_generated/presentation/screens/product_details/product_details_r
@TypedGoRoute<ProductDetailsRoute>(path: AppRoutes.productDetails)
class ProductDetailsRoute extends GoRouteData with $ProductDetailsRoute {
final int id;
- final bool isEdit;
- const ProductDetailsRoute(this.id, {this.isEdit = false});
+ const ProductDetailsRoute(this.id);
@override
Page<void> buildPage(BuildContext context, GoRouterState state) {
@@ -24,7 +23,6 @@ class ProductDetailsRoute extends GoRouteData with $ProductDetailsRoute {
context.read(),
context.read(),
productId: id,
- isEdit: isEdit,
),
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 d7e9cfb5..7d8732e8 100644
--- a/concierge/lib/presentation/screens/product_details/product_details_screen.dart
+++ b/concierge/lib/presentation/screens/product_details/product_details_screen.dart
@@ -1,5 +1,6 @@
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/bevelled_app_bar.dart';
import 'package:concierge/presentation/widgets/padded_column.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@@ -89,23 +90,7 @@ class ProductDetailsScreen extends StatelessWidget {
);
},
),
- 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: [
- Spacer(),
- CloseButton(color: Colors.black),
- ],
- ),
- ),
- ),
+ BevelledAppBar(),
],
),
),
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
index c09fa2f8..7c16f673 100644
--- 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
@@ -1,4 +1,3 @@
-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';
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 378a9a96..3ef91a12 100644
--- a/concierge/lib/presentation/screens/review_order/review_order_screen.dart
+++ b/concierge/lib/presentation/screens/review_order/review_order_screen.dart
@@ -3,6 +3,7 @@ import 'package:concierge/presentation/app/cart_cubit.dart';
import 'package:concierge/presentation/screens/review_order/widget/delivery_method_picker.dart';
import 'package:concierge/presentation/screens/review_order/widget/product_in_cart_list_tile.dart';
import 'package:concierge/presentation/screens/review_order/widget/review_order_app_bar.dart';
+import 'package:concierge/presentation/widgets/bevelled_app_bar.dart';
import 'package:concierge/presentation/widgets/padded_column.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@@ -27,21 +28,25 @@ class ReviewOrderScreen extends StatelessWidget {
),
],
child: Scaffold(
- appBar: AppBar(),
bottomNavigationBar: ReviewOrderAppBar(),
- body: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- mainAxisAlignment: MainAxisAlignment.start,
+ body: Stack(
children: [
- Text("ReviewOrder"),
- const DeliveryMethodPicker(),
- PaddedColumn(
- padding: EdgeInsets.symmetric(horizontal: 16),
- children: buildProducts(context, cartCubit.products).toList(),
+ SingleChildScrollView(
+ child: PaddedColumn(
+ padding: EdgeInsets.symmetric(horizontal: 16),
+ children: [
+ Gap(48),
+ Text("Din Bestilling", style: TextStyle(fontSize: 20)),
+ Gap(48),
+ if (cartCubit.products.isNotEmpty) const DeliveryMethodPicker(),
+ ...buildProducts(context, cartCubit.products),
+ Gap(36),
+ if (cartCubit.products.isNotEmpty) ...buildCommentField(context),
+ Gap(36),
+ ],
+ ),
),
- Gap(16),
- Text("Kommentar til ordre"),
- TextField(),
+ BevelledAppBar(),
],
),
),
@@ -50,13 +55,40 @@ class ReviewOrderScreen extends StatelessWidget {
);
}
+ Iterable<Widget> buildCommentField(BuildContext context) sync* {
+ final cartCubit = context.watch<CartCubit>();
+ yield Text("Kommentar til ordre", style: TextStyle(fontSize: 20));
+ yield Gap(16);
+ yield TextField(
+ controller: cartCubit.orderCommentController,
+ maxLines: 3,
+ decoration: InputDecoration(
+ focusedBorder: OutlineInputBorder(
+ borderSide: BorderSide(color: Colors.grey, width: 1),
+ borderRadius: BorderRadius.circular(12),
+ ),
+ border: OutlineInputBorder(
+ borderSide: BorderSide(color: Colors.grey, width: 1),
+ borderRadius: BorderRadius.circular(12),
+ ),
+ ),
+ );
+ }
+
Iterable<Widget> buildProducts(BuildContext context, Iterable<Product> products) sync* {
- yield Divider();
+ if (products.isEmpty) {
+ yield Gap(36);
+ yield Center(
+ child: Text("Du har ingen varer i kurven."),
+ );
+ } else {
+ yield Divider(color: Colors.grey.shade300);
+ }
for (final product in products) {
- Gap(12);
+ Gap(16);
yield ProductInCartListTile(product: product);
- Gap(12);
- yield Divider();
+ Gap(16);
+ yield Divider(color: Colors.grey.shade300);
}
}
}
diff --git a/concierge/lib/presentation/screens/review_order/widget/delivery_method_picker.dart b/concierge/lib/presentation/screens/review_order/widget/delivery_method_picker.dart
index 3077bd8e..dbc28aae 100644
--- a/concierge/lib/presentation/screens/review_order/widget/delivery_method_picker.dart
+++ b/concierge/lib/presentation/screens/review_order/widget/delivery_method_picker.dart
@@ -3,6 +3,7 @@ import 'package:concierge/presentation/app/cart_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';
class DeliveryMethodPicker extends StatelessWidget {
const DeliveryMethodPicker({super.key});
@@ -12,30 +13,59 @@ class DeliveryMethodPicker extends StatelessWidget {
final cartCubit = context.watch<CartCubit>();
return Row(
children: [
- TextButton(
- onPressed: () {
- cartCubit.updateDelivery(DeliveryMethod.delivery("16:00"));
- },
- child: Text(
- "Levering",
- style: TextStyle(
- color: cartCubit.state.deliveryMethod is Delivery ? AppColors.sandColor : null,
- ),
+ Expanded(
+ child: buildButton(
+ context,
+ isSelected: cartCubit.state.deliveryMethod is Delivery,
+ onClick: () {
+ cartCubit.updateDelivery(DeliveryMethod.delivery("00:00"));
+ },
+ text: "Levering",
),
),
- Spacer(),
- TextButton(
- onPressed: () {
- cartCubit.updateDelivery(DeliveryMethod.pickUp);
- },
- child: Text(
- "Afhentning",
- style: TextStyle(
- color: cartCubit.state.deliveryMethod is PickUp ? AppColors.sandColor : null,
- ),
+ const Gap(8),
+ Expanded(
+ child: buildButton(
+ context,
+ isSelected: cartCubit.state.deliveryMethod is PickUp,
+ onClick: () {
+ cartCubit.updateDelivery(DeliveryMethod.pickUp);
+ },
+ text: "Afhentning",
),
),
],
);
}
+
+ Widget buildButton(
+ BuildContext context, {
+ required bool isSelected,
+ required VoidCallback onClick,
+ required String text,
+ }) {
+ final Color bgColor;
+ final BorderSide? border;
+ final Color textColor;
+ if (isSelected) {
+ bgColor = AppColors.sandColor;
+ textColor = Colors.white;
+ border = null;
+ } else {
+ bgColor = Colors.white;
+ textColor = Colors.black;
+ border = BorderSide(color: Colors.grey.shade300, width: 1);
+ }
+ return TextButton(
+ style: ButtonStyle(
+ backgroundColor: WidgetStatePropertyAll(bgColor),
+ side: WidgetStatePropertyAll(border),
+ ),
+ onPressed: onClick,
+ child: Text(
+ text,
+ style: TextStyle(color: textColor),
+ ),
+ );
+ }
}
diff --git a/concierge/lib/presentation/screens/review_order/widget/product_in_cart_list_tile.dart b/concierge/lib/presentation/screens/review_order/widget/product_in_cart_list_tile.dart
index d73d1c3f..ce835a58 100644
--- a/concierge/lib/presentation/screens/review_order/widget/product_in_cart_list_tile.dart
+++ b/concierge/lib/presentation/screens/review_order/widget/product_in_cart_list_tile.dart
@@ -3,6 +3,7 @@ import 'package:concierge/presentation/app/cart_cubit.dart';
import 'package:concierge/presentation/screens/product_details/product_details_route.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:gap/gap.dart';
class ProductInCartListTile extends StatelessWidget {
const ProductInCartListTile({super.key, required this.product});
@@ -15,31 +16,46 @@ class ProductInCartListTile extends StatelessWidget {
final quantity = cartCubit.getQuantity(product.id);
final price = product.price * quantity;
- return Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
- crossAxisAlignment: CrossAxisAlignment.end,
- children: [
- Image.network(
- product.images.first.url,
- height: 80,
- width: 80,
- ),
- Column(
- mainAxisSize: MainAxisSize.min,
- crossAxisAlignment: CrossAxisAlignment.start,
- mainAxisAlignment: MainAxisAlignment.start,
- children: [
- Text("${quantity}x${product.title}"),
- Text("$price kr."),
- ],
- ),
- TextButton(
- onPressed: () {
- ProductDetailsRoute(product.id, isEdit: true).push(context);
- },
- child: Text("Rediger"),
- ),
- ],
+ return SizedBox(
+ height: 80,
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ crossAxisAlignment: CrossAxisAlignment.end,
+ children: [
+ Container(
+ width: 80,
+ decoration: BoxDecoration(
+ borderRadius: BorderRadius.circular(16),
+ image: DecorationImage(
+ image: NetworkImage(product.images.first.url),
+ fit: BoxFit.cover,
+ ),
+ ),
+ ),
+ const Gap(12),
+ Expanded(
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ mainAxisAlignment: MainAxisAlignment.start,
+ children: [
+ Text("${quantity}x ${product.title}", maxLines: 2, overflow: TextOverflow.ellipsis,),
+ Spacer(),
+ Text("$price kr."),
+ ],
+ ),
+ ),
+ TextButton(
+ style: ButtonStyle(
+ visualDensity: VisualDensity.compact
+ ),
+ onPressed: () {
+ ProductDetailsRoute(product.id).push(context);
+ },
+ child: Text("Rediger"),
+ ),
+ ],
+ ),
);
}
}
diff --git a/concierge/lib/presentation/screens/review_order/widget/review_order_app_bar.dart b/concierge/lib/presentation/screens/review_order/widget/review_order_app_bar.dart
index b340a9cb..dafc4678 100644
--- a/concierge/lib/presentation/screens/review_order/widget/review_order_app_bar.dart
+++ b/concierge/lib/presentation/screens/review_order/widget/review_order_app_bar.dart
@@ -1,5 +1,6 @@
import 'package:concierge/presentation/app/cart_cubit.dart';
import 'package:concierge/presentation/screens/provide_location/provide_location_route.dart';
+import 'package:concierge/presentation/theme/app_colors.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@@ -11,13 +12,19 @@ class ReviewOrderAppBar extends StatelessWidget {
final cartCubit = context.read<CartCubit>();
return Padding(
padding: const EdgeInsets.all(16.0),
- child: Container(
+ child: SizedBox(
height: 100,
child: SafeArea(
child: ElevatedButton(
- onPressed: () {
- ProvideLocationRoute().push(context);
- },
+ style: ButtonStyle(
+ backgroundColor: WidgetStateMapper({
+ WidgetState.disabled: Colors.grey,
+ WidgetState.any: AppColors.sandColor,
+ }),
+ ),
+ onPressed: cartCubit.products.isEmpty
+ ? null
+ : () => ProvideLocationRoute().push(context),
child: Row(
children: [
Text(
diff --git a/concierge/lib/presentation/widgets/bevelled_app_bar.dart b/concierge/lib/presentation/widgets/bevelled_app_bar.dart
new file mode 100644
index 00000000..8d6df388
--- /dev/null
+++ b/concierge/lib/presentation/widgets/bevelled_app_bar.dart
@@ -0,0 +1,27 @@
+import 'package:concierge/presentation/theme/app_colors.dart';
+import 'package:flutter/material.dart';
+
+class BevelledAppBar extends StatelessWidget {
+ const BevelledAppBar({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return Container(
+ height: 100,
+ decoration: BoxDecoration(
+ color: AppColors.colorSecondary,
+ borderRadius: BorderRadius.vertical(bottom: Radius.circular(24)),
+ ),
+ child: Padding(
+ padding: const EdgeInsets.all(8.0),
+ child: Row(
+ crossAxisAlignment: CrossAxisAlignment.end,
+ children: [
+ Spacer(),
+ CloseButton(color: Colors.black),
+ ],
+ ),
+ ),
+ );
+ }
+}