diff --git a/lib/features/cart/domain/models/cart_item_data.dart b/lib/features/cart/domain/models/cart_item_data.dart index 33ba649..dcbfcfc 100644 --- a/lib/features/cart/domain/models/cart_item_data.dart +++ b/lib/features/cart/domain/models/cart_item_data.dart @@ -8,6 +8,7 @@ class CartItemData { required this.normalPrice, required this.offerPrice, required this.suitableFor, + required this.businessId, this.quantity = 1, }); final String id; @@ -16,11 +17,13 @@ class CartItemData { final double normalPrice; final double offerPrice; final List suitableFor; + final String businessId; int quantity; CartItemData copyWith({ int? quantity, List? suitableFor, + String? businessId, }) { return CartItemData( id: id, @@ -30,6 +33,7 @@ class CartItemData { offerPrice: offerPrice, quantity: quantity ?? this.quantity, suitableFor: suitableFor ?? this.suitableFor, + businessId: businessId ?? this.businessId, ); } } diff --git a/lib/features/cart/presentation/bloc/cart_bloc.dart b/lib/features/cart/presentation/bloc/cart_bloc.dart index 2fdf4b1..b3fa7ef 100644 --- a/lib/features/cart/presentation/bloc/cart_bloc.dart +++ b/lib/features/cart/presentation/bloc/cart_bloc.dart @@ -2,19 +2,28 @@ import 'package:eco_bites/core/blocs/resettable_mixin.dart'; import 'package:eco_bites/features/cart/domain/models/cart_item_data.dart'; import 'package:eco_bites/features/cart/presentation/bloc/cart_event.dart'; import 'package:eco_bites/features/cart/presentation/bloc/cart_state.dart'; +import 'package:eco_bites/features/orders/data/models/order_model.dart'; +import 'package:eco_bites/features/orders/domain/entities/order.dart'; +import 'package:eco_bites/features/orders/presentation/bloc/order_bloc.dart'; +import 'package:eco_bites/features/orders/presentation/bloc/order_event.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class CartBloc extends Bloc with ResettableMixin { - CartBloc(List initialItems) - : super(CartState(items: initialItems)) { + CartBloc({ + required this.orderBloc, + required List initialItems, + }) : super(CartState(items: initialItems)) { on(_onAddToCart); on(_onCartItemQuantityChanged); on(_onCartItemRemoved); on(_onClearCart); on(_onCompletePurchase); + on(_onPurchaseCart); } + final OrderBloc orderBloc; + void _onClearCart(ClearCart event, Emitter emit) { emit( const CartState(items: []), @@ -74,6 +83,52 @@ class CartBloc extends Bloc emit(CartState(items: updatedItems)); } + Future _onPurchaseCart( + PurchaseCartEvent event, + Emitter emit, + ) async { + if (state.items.isEmpty) { + emit(state.copyWith(items: state.items, error: 'Cart is empty')); + return; + } + + try { + final List orderItems = + state.items.map((CartItemData cartItem) { + return OrderItemModel( + id: cartItem.id, + name: cartItem.title, + quantity: cartItem.quantity, + price: cartItem.offerPrice, + ); + }).toList(); + + final double totalAmount = state.items.fold( + 0, + (double sum, CartItemData item) => + sum + (item.offerPrice * item.quantity), + ); + + final OrderModel order = OrderModel( + id: '', // Will be set by Firestore + businessId: state.items.first.businessId, + items: orderItems, + totalAmount: totalAmount, + status: OrderStatus.pending, + createdAt: DateTime.now(), + ); + + orderBloc.add(CreateOrderEvent(order: order)); + + // Clear cart after successful purchase + emit(const CartState(items: [])); + } catch (e) { + emit( + state.copyWith(items: state.items, error: 'Failed to create order: $e'), + ); + } + } + @override void reset() { // ignore: invalid_use_of_visible_for_testing_member diff --git a/lib/features/cart/presentation/bloc/cart_event.dart b/lib/features/cart/presentation/bloc/cart_event.dart index d0c1735..dc0d953 100644 --- a/lib/features/cart/presentation/bloc/cart_event.dart +++ b/lib/features/cart/presentation/bloc/cart_event.dart @@ -36,3 +36,7 @@ class AddToCart extends CartEvent { class ClearCart extends CartEvent {} class CompletePurchase extends CartEvent {} + +class PurchaseCartEvent extends CartEvent { + const PurchaseCartEvent(); +} diff --git a/lib/features/cart/presentation/bloc/cart_state.dart b/lib/features/cart/presentation/bloc/cart_state.dart index 870ef56..da75068 100644 --- a/lib/features/cart/presentation/bloc/cart_state.dart +++ b/lib/features/cart/presentation/bloc/cart_state.dart @@ -2,11 +2,14 @@ import 'package:eco_bites/features/cart/domain/models/cart_item_data.dart'; import 'package:equatable/equatable.dart'; class CartState extends Equatable { - const CartState({required this.items}); + const CartState({ + required this.items, + this.error, + }); final List items; + final String? error; - // Calculate subtotal as the sum of offer prices times quantity for each item double get subtotal => items.fold( 0.0, (double sum, CartItemData item) => @@ -14,11 +17,15 @@ class CartState extends Equatable { ); @override - List get props => [items]; + List get props => [items, error]; - CartState copyWith({List? items}) { + CartState copyWith({ + List? items, + String? error, + }) { return CartState( items: items ?? this.items, + error: error, ); } } diff --git a/lib/features/cart/presentation/screens/cart_screen.dart b/lib/features/cart/presentation/screens/cart_screen.dart index 0b68c9a..71edb61 100644 --- a/lib/features/cart/presentation/screens/cart_screen.dart +++ b/lib/features/cart/presentation/screens/cart_screen.dart @@ -71,12 +71,22 @@ class CartScreenContent extends StatelessWidget { ), ElevatedButton( onPressed: () { - context.read().add(CompletePurchase()); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Purchase Completed')), - ); + if (state.items.isEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Cart is empty'), + ), + ); + } else { + context.read().add(const PurchaseCartEvent()); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Order placed successfully!'), + ), + ); + } }, - child: const Text('Complete Purchase'), + child: const Text('Purchase'), ), ], ), diff --git a/lib/features/food/domain/entities/offer.dart b/lib/features/food/domain/entities/offer.dart index 24ce0b7..6060a42 100644 --- a/lib/features/food/domain/entities/offer.dart +++ b/lib/features/food/domain/entities/offer.dart @@ -49,6 +49,7 @@ extension OfferExtensions on Offer { offerPrice: offerPrice, imageUrl: imageUrl, suitableFor: suitableFor, + businessId: businessId, ); } } diff --git a/lib/features/orders/data/datasources/order_remote_data_source.dart b/lib/features/orders/data/datasources/order_remote_data_source.dart index 97b4cb8..a687c87 100644 --- a/lib/features/orders/data/datasources/order_remote_data_source.dart +++ b/lib/features/orders/data/datasources/order_remote_data_source.dart @@ -1,8 +1,8 @@ import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:eco_bites/core/error/exceptions.dart'; +import 'package:eco_bites/core/utils/user_util.dart'; import 'package:eco_bites/features/orders/data/models/order_model.dart'; import 'package:eco_bites/features/orders/domain/entities/order.dart'; -import 'package:firebase_auth/firebase_auth.dart'; import 'package:logger/logger.dart'; abstract class OrderRemoteDataSource { @@ -14,17 +14,14 @@ abstract class OrderRemoteDataSource { class OrderRemoteDataSourceImpl implements OrderRemoteDataSource { const OrderRemoteDataSourceImpl({ required FirebaseFirestore firestore, - required FirebaseAuth auth, - }) : _firestore = firestore, - _auth = auth; + }) : _firestore = firestore; final FirebaseFirestore _firestore; - final FirebaseAuth _auth; @override Future> getOrders() async { try { - final String? userId = _auth.currentUser?.uid; + final String? userId = await getUserId(); if (userId == null) { throw const AuthException('User not authenticated'); } @@ -66,6 +63,41 @@ class OrderRemoteDataSourceImpl implements OrderRemoteDataSource { @override Future createOrder(OrderModel order) async { - await _firestore.collection('orders').doc().set(order.toMap()); + try { + final String? userId = await getUserId(); + if (userId == null) { + throw const AuthException('User not authenticated'); + } + + // validate if the order items are not empty + if (order.items.isEmpty) { + Logger().e('Order items cannot be empty'); + // do not throw, snackbar will handle this + return; + } + + // Get business name from Firestore + final DocumentSnapshot> businessDoc = + await _firestore + .collection('foodBusiness') + .doc(order.businessId) + .get(); + + if (!businessDoc.exists) { + Logger().e('Business not found'); + } + + final String businessName = businessDoc.data()?['name'] as String; + + // Create the order with the fetched business name + final Map orderData = order.toMap() + ..['userId'] = userId + ..['businessName'] = businessName; + + await _firestore.collection('orders').doc().set(orderData); + } catch (e) { + Logger().e('Error creating order: $e'); + throw AuthException('Failed to create order: $e'); + } } } diff --git a/lib/features/orders/data/models/order_model.dart b/lib/features/orders/data/models/order_model.dart index e8f1cb7..79d2225 100644 --- a/lib/features/orders/data/models/order_model.dart +++ b/lib/features/orders/data/models/order_model.dart @@ -6,13 +6,15 @@ class OrderModel extends order_entity.Order { const OrderModel({ required super.id, required super.businessId, - required super.businessName, + String? businessName, required super.items, required super.totalAmount, required super.status, required super.createdAt, super.completedAt, - }); + }) : super( + businessName: businessName ?? '', + ); factory OrderModel.fromMap(Map map, String id) { return OrderModel( diff --git a/lib/features/orders/data/repositories/order_repository_impl.dart b/lib/features/orders/data/repositories/order_repository_impl.dart index f271bd2..6bf2c01 100644 --- a/lib/features/orders/data/repositories/order_repository_impl.dart +++ b/lib/features/orders/data/repositories/order_repository_impl.dart @@ -73,4 +73,20 @@ class OrderRepositoryImpl implements OrderRepository { ); } } + + @override + Future> createOrder(OrderModel order) async { + if (await networkInfo.isConnected) { + try { + await remoteDataSource.createOrder(order); + return const Right(null); + } on AuthException catch (e) { + return Left(AuthFailure(e.message)); + } + } else { + return const Left( + NetworkFailure('No internet connection'), + ); + } + } } diff --git a/lib/features/orders/domain/repositories/order_repository.dart b/lib/features/orders/domain/repositories/order_repository.dart index a699cfc..1062fdd 100644 --- a/lib/features/orders/domain/repositories/order_repository.dart +++ b/lib/features/orders/domain/repositories/order_repository.dart @@ -1,5 +1,6 @@ import 'package:dartz/dartz.dart'; import 'package:eco_bites/core/error/failures.dart'; +import 'package:eco_bites/features/orders/data/models/order_model.dart'; import 'package:eco_bites/features/orders/domain/entities/order.dart' as entities; @@ -9,4 +10,5 @@ abstract class OrderRepository { String orderId, entities.OrderStatus newStatus, ); + Future> createOrder(OrderModel order); } diff --git a/lib/features/orders/domain/usecases/create_order.dart b/lib/features/orders/domain/usecases/create_order.dart new file mode 100644 index 0000000..19f8da6 --- /dev/null +++ b/lib/features/orders/domain/usecases/create_order.dart @@ -0,0 +1,24 @@ +import 'package:dartz/dartz.dart'; +import 'package:eco_bites/core/error/failures.dart'; +import 'package:eco_bites/features/orders/data/models/order_model.dart'; +import 'package:eco_bites/features/orders/domain/repositories/order_repository.dart'; +import 'package:equatable/equatable.dart'; + +class CreateOrder { + const CreateOrder(this.repository); + + final OrderRepository repository; + + Future> call(CreateOrderParams params) async { + return repository.createOrder(params.order); + } +} + +class CreateOrderParams extends Equatable { + const CreateOrderParams({required this.order}); + + final OrderModel order; + + @override + List get props => [order]; +} diff --git a/lib/features/orders/presentation/bloc/order_bloc.dart b/lib/features/orders/presentation/bloc/order_bloc.dart index 9c3040c..7ee2122 100644 --- a/lib/features/orders/presentation/bloc/order_bloc.dart +++ b/lib/features/orders/presentation/bloc/order_bloc.dart @@ -3,6 +3,7 @@ import 'package:eco_bites/core/blocs/resettable_mixin.dart'; import 'package:eco_bites/core/error/failures.dart'; import 'package:eco_bites/features/orders/domain/entities/order.dart' as order_entity; +import 'package:eco_bites/features/orders/domain/usecases/create_order.dart'; import 'package:eco_bites/features/orders/domain/usecases/get_orders.dart'; import 'package:eco_bites/features/orders/domain/usecases/update_order_status.dart'; import 'package:eco_bites/features/orders/presentation/bloc/order_event.dart'; @@ -15,15 +16,19 @@ class OrderBloc extends Bloc OrderBloc({ required GetOrders getOrders, required UpdateOrderStatus updateOrderStatus, + required CreateOrder createOrder, }) : _getOrders = getOrders, _updateOrderStatus = updateOrderStatus, + _createOrder = createOrder, super(OrdersLoading()) { on(_onLoadOrders); on(_onUpdateOrder); + on(_onCreateOrder); } final GetOrders _getOrders; final UpdateOrderStatus _updateOrderStatus; + final CreateOrder _createOrder; Future _onLoadOrders(LoadOrders event, Emitter emit) async { emit(OrdersLoading()); @@ -70,6 +75,32 @@ class OrderBloc extends Bloc } } + Future _onCreateOrder( + CreateOrderEvent event, + Emitter emit, + ) async { + Logger().d('Creating order'); + emit(OrdersLoading()); + + final Either result = await _createOrder( + CreateOrderParams(order: event.order), + ); + + Logger().d('Order created'); + + result.fold( + (Failure failure) { + Logger().e('Failed to create order: ${failure.message}'); + emit(OrdersError(failure.message)); + }, + (_) { + Logger().d('Order created successfully'); + // Reload orders after successful creation + add(LoadOrders()); + }, + ); + } + @override void reset() { // ignore: invalid_use_of_visible_for_testing_member diff --git a/lib/features/orders/presentation/bloc/order_event.dart b/lib/features/orders/presentation/bloc/order_event.dart index da5e72a..7228254 100644 --- a/lib/features/orders/presentation/bloc/order_event.dart +++ b/lib/features/orders/presentation/bloc/order_event.dart @@ -1,3 +1,4 @@ +import 'package:eco_bites/features/orders/data/models/order_model.dart'; import 'package:eco_bites/features/orders/domain/entities/order.dart'; import 'package:equatable/equatable.dart'; @@ -22,3 +23,12 @@ class UpdateOrder extends OrderEvent { @override List get props => [orderId, newStatus]; } + +class CreateOrderEvent extends OrderEvent { + const CreateOrderEvent({required this.order}); + + final OrderModel order; + + @override + List get props => [order]; +} diff --git a/lib/injection_container.dart b/lib/injection_container.dart index c7a87c0..54d8fc1 100644 --- a/lib/injection_container.dart +++ b/lib/injection_container.dart @@ -31,6 +31,7 @@ import 'package:eco_bites/features/orders/data/datasources/order_local_data_sour import 'package:eco_bites/features/orders/data/datasources/order_remote_data_source.dart'; import 'package:eco_bites/features/orders/data/repositories/order_repository_impl.dart'; import 'package:eco_bites/features/orders/domain/repositories/order_repository.dart'; +import 'package:eco_bites/features/orders/domain/usecases/create_order.dart'; import 'package:eco_bites/features/orders/domain/usecases/get_orders.dart'; import 'package:eco_bites/features/orders/domain/usecases/update_order_status.dart'; import 'package:eco_bites/features/orders/presentation/bloc/order_bloc.dart'; @@ -189,6 +190,7 @@ void _setupOrderFeature() { () => OrderBloc( getOrders: serviceLocator(), updateOrderStatus: serviceLocator(), + createOrder: serviceLocator(), ), ); @@ -196,6 +198,7 @@ void _setupOrderFeature() { serviceLocator.registerLazySingleton(() => GetOrders(serviceLocator())); serviceLocator .registerLazySingleton(() => UpdateOrderStatus(serviceLocator())); + serviceLocator.registerLazySingleton(() => CreateOrder(serviceLocator())); // Repository serviceLocator.registerLazySingleton( @@ -210,7 +213,6 @@ void _setupOrderFeature() { serviceLocator.registerLazySingleton( () => OrderRemoteDataSourceImpl( firestore: serviceLocator(), - auth: serviceLocator(), ), ); serviceLocator.registerLazySingleton( @@ -220,7 +222,10 @@ void _setupOrderFeature() { void _setupCartFeature() { serviceLocator.registerFactory( - () => CartBloc([]), + () => CartBloc( + orderBloc: serviceLocator(), + initialItems: [], + ), ); } diff --git a/pubspec.lock b/pubspec.lock index aab6544..e5f0928 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1145,10 +1145,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" + sha256: f652077d0bdf60abe4c1f6377448e8655008eef28f128bc023f7b5e8dfeb48fc url: "https://pub.dev" source: hosted - version: "14.2.5" + version: "14.2.4" web: dependency: transitive description: