diff --git a/lib/core/utils/date_utils.dart b/lib/core/utils/date_utils.dart index a56464f..5a28fcc 100644 --- a/lib/core/utils/date_utils.dart +++ b/lib/core/utils/date_utils.dart @@ -2,7 +2,6 @@ import 'package:intl/intl.dart'; /// Utility class for date related operations. class DateUtils { - DateUtils._(); /// Formats the given date into a string in the format "MMM d, y". @@ -15,7 +14,10 @@ class DateUtils { /// Formats the given date to show only the day and month. /// Example output: "04/27" static String formatDayMonth(DateTime date) { - final DateFormat formatter = DateFormat('MM/dd'); - return formatter.format(date); + return DateFormat('d MMM').format(date); + } + + static String formatDateTime(DateTime dateTime) { + return DateFormat('d MMM y, HH:mm').format(dateTime); } } diff --git a/lib/features/orders/data/datasources/order_local_data_source.dart b/lib/features/orders/data/datasources/order_local_data_source.dart new file mode 100644 index 0000000..7932cd2 --- /dev/null +++ b/lib/features/orders/data/datasources/order_local_data_source.dart @@ -0,0 +1,192 @@ +import 'package:eco_bites/core/error/exceptions.dart'; +import 'package:eco_bites/features/orders/data/models/order_model.dart'; +import 'package:eco_bites/features/orders/domain/entities/order.dart'; +import 'package:logger/logger.dart'; +import 'package:path/path.dart'; +import 'package:sqflite/sqflite.dart'; + +abstract class OrderLocalDataSource { + Future cacheOrders(List orders); + Future> getCachedOrders(); + Future clearCache(); + Future updateOrderStatus(String orderId, OrderStatus newStatus); +} + +class OrderLocalDataSourceImpl implements OrderLocalDataSource { + Database? _database; + + Future get database async { + _database ??= await _initDatabase(); + return _database!; + } + + Future _initDatabase() async { + final String path = join(await getDatabasesPath(), 'orders.db'); + return openDatabase( + path, + version: 1, + onCreate: (Database db, int version) async { + // Create orders table + await db.execute(''' + CREATE TABLE orders( + id TEXT PRIMARY KEY, + businessId TEXT NOT NULL, + businessName TEXT NOT NULL, + totalAmount REAL NOT NULL, + status TEXT NOT NULL, + createdAt TEXT NOT NULL, + completedAt TEXT + ) + '''); + + // Create order items table with foreign key to orders + await db.execute(''' + CREATE TABLE order_items( + id TEXT PRIMARY KEY, + orderId TEXT NOT NULL, + name TEXT NOT NULL, + quantity INTEGER NOT NULL, + price REAL NOT NULL, + FOREIGN KEY (orderId) REFERENCES orders (id) + ) + '''); + }, + ); + } + + @override + Future cacheOrders(List orders) async { + try { + final Database db = await database; + await db.transaction((Transaction txn) async { + // Clear existing data + await txn.delete('order_items'); + await txn.delete('orders'); + + // Insert new data + for (final OrderModel order in orders) { + await txn.insert( + 'orders', + { + 'id': order.id, + 'businessId': order.businessId, + 'businessName': order.businessName, + 'totalAmount': order.totalAmount, + 'status': order.status.name, + 'createdAt': order.createdAt.toIso8601String(), + if (order.completedAt != null) + 'completedAt': order.completedAt!.toIso8601String(), + }, + conflictAlgorithm: ConflictAlgorithm.replace, + ); + + // Insert order items + for (final OrderItem item in order.items) { + await txn.insert( + 'order_items', + { + 'id': item.id, + 'orderId': order.id, + 'name': item.name, + 'quantity': item.quantity, + 'price': item.price, + }, + conflictAlgorithm: ConflictAlgorithm.replace, + ); + } + } + }); + } catch (e) { + Logger().e('Error caching orders: $e'); + throw const CacheException('Failed to cache orders'); + } + } + + @override + Future> getCachedOrders() async { + try { + final Database db = await database; + + // Log the database path + Logger().d('Database path: ${await getDatabasesPath()}'); + + // Get all orders + final List> orderMaps = await db.query('orders'); + Logger().d('Number of cached orders: ${orderMaps.length}'); + + return Future.wait( + orderMaps.map((Map orderMap) async { + // Get items for this order + final List> itemMaps = await db.query( + 'order_items', + where: 'orderId = ?', + whereArgs: [orderMap['id'] as String], + ); + + final List items = itemMaps + .map( + (Map itemMap) => OrderItemModel( + id: itemMap['id'] as String, + name: itemMap['name'] as String, + quantity: itemMap['quantity'] as int, + price: itemMap['price'] as double, + ), + ) + .toList(); + + return OrderModel( + id: orderMap['id'] as String, + businessId: orderMap['businessId'] as String, + businessName: orderMap['businessName'] as String, + items: items, + totalAmount: orderMap['totalAmount'] as double, + status: OrderStatus.values.firstWhere( + (OrderStatus s) => s.name == (orderMap['status'] as String), + ), + createdAt: DateTime.parse(orderMap['createdAt'] as String), + completedAt: orderMap['completedAt'] != null + ? DateTime.parse(orderMap['completedAt'] as String) + : null, + ); + }), + ); + } catch (e) { + Logger().e('Error getting cached orders: $e'); + throw const CacheException('Failed to get cached orders'); + } + } + + @override + Future updateOrderStatus(String orderId, OrderStatus newStatus) async { + try { + final Database db = await database; + await db.update( + 'orders', + { + 'status': newStatus.name, + if (newStatus == OrderStatus.completed) + 'completedAt': DateTime.now().toIso8601String(), + }, + where: 'id = ?', + whereArgs: [orderId], + ); + } catch (e) { + Logger().e('Error updating order status: $e'); + throw const CacheException('Failed to update order status'); + } + } + + @override + Future clearCache() async { + try { + final Database db = await database; + await db.transaction((Transaction txn) async { + await txn.delete('order_items'); + await txn.delete('orders'); + }); + } catch (e) { + Logger().e('Error clearing cache: $e'); + throw const CacheException('Failed to clear cache'); + } + } +} diff --git a/lib/features/orders/data/datasources/order_remote_data_source.dart b/lib/features/orders/data/datasources/order_remote_data_source.dart new file mode 100644 index 0000000..97b4cb8 --- /dev/null +++ b/lib/features/orders/data/datasources/order_remote_data_source.dart @@ -0,0 +1,71 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:eco_bites/core/error/exceptions.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 { + Future> getOrders(); + Future updateOrderStatus(String orderId, OrderStatus newStatus); + Future createOrder(OrderModel order); +} + +class OrderRemoteDataSourceImpl implements OrderRemoteDataSource { + const OrderRemoteDataSourceImpl({ + required FirebaseFirestore firestore, + required FirebaseAuth auth, + }) : _firestore = firestore, + _auth = auth; + + final FirebaseFirestore _firestore; + final FirebaseAuth _auth; + + @override + Future> getOrders() async { + try { + final String? userId = _auth.currentUser?.uid; + if (userId == null) { + throw const AuthException('User not authenticated'); + } + + final QuerySnapshot> snapshot = await _firestore + .collection('orders') + .where('userId', isEqualTo: userId) + .orderBy('createdAt', descending: true) + .get(); + + return snapshot.docs + .map( + (QueryDocumentSnapshot> doc) => + OrderModel.fromMap(doc.data(), doc.id), + ) + .toList(); + } catch (e) { + Logger().e('Error fetching orders: $e'); + throw AuthException('Failed to fetch orders: $e'); + } + } + + @override + Future updateOrderStatus(String orderId, OrderStatus newStatus) async { + try { + await _firestore + .collection('orders') + .doc(orderId) + .update({ + 'status': newStatus.name, + if (newStatus == OrderStatus.completed) + 'completedAt': FieldValue.serverTimestamp(), + }); + } catch (e) { + Logger().e('Error updating order status: $e'); + throw AuthException('Failed to update order status: $e'); + } + } + + @override + Future createOrder(OrderModel order) async { + await _firestore.collection('orders').doc().set(order.toMap()); + } +} diff --git a/lib/features/orders/data/models/order_model.dart b/lib/features/orders/data/models/order_model.dart new file mode 100644 index 0000000..e8f1cb7 --- /dev/null +++ b/lib/features/orders/data/models/order_model.dart @@ -0,0 +1,81 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:eco_bites/features/orders/domain/entities/order.dart' + as order_entity; + +class OrderModel extends order_entity.Order { + const OrderModel({ + required super.id, + required super.businessId, + required super.businessName, + required super.items, + required super.totalAmount, + required super.status, + required super.createdAt, + super.completedAt, + }); + + factory OrderModel.fromMap(Map map, String id) { + return OrderModel( + id: id, + businessId: map['businessId'] as String, + businessName: map['businessName'] as String, + items: (map['items'] as List) + .map( + (dynamic item) => + OrderItemModel.fromMap(item as Map), + ) + .toList(), + totalAmount: (map['totalAmount'] as num).toDouble(), + status: order_entity.OrderStatus.values.firstWhere( + (order_entity.OrderStatus s) => s.name == (map['status'] as String), + ), + createdAt: (map['createdAt'] as Timestamp).toDate(), + completedAt: map['completedAt'] != null + ? (map['completedAt'] as Timestamp).toDate() + : null, + ); + } + + Map toMap() { + return { + 'businessId': businessId, + 'businessName': businessName, + 'items': items + .map( + (order_entity.OrderItem item) => (item as OrderItemModel).toMap(), + ) + .toList(), + 'totalAmount': totalAmount, + 'status': status.name, + 'createdAt': Timestamp.fromDate(createdAt), + if (completedAt != null) 'completedAt': Timestamp.fromDate(completedAt!), + }; + } +} + +class OrderItemModel extends order_entity.OrderItem { + const OrderItemModel({ + required super.id, + required super.name, + required super.quantity, + required super.price, + }); + + factory OrderItemModel.fromMap(Map map) { + return OrderItemModel( + id: map['id'] as String, + name: map['name'] as String, + quantity: map['quantity'] as int, + price: (map['price'] as num).toDouble(), + ); + } + + Map toMap() { + return { + 'id': id, + 'name': name, + 'quantity': quantity, + 'price': price, + }; + } +} diff --git a/lib/features/orders/data/repositories/order_repository_impl.dart b/lib/features/orders/data/repositories/order_repository_impl.dart new file mode 100644 index 0000000..f271bd2 --- /dev/null +++ b/lib/features/orders/data/repositories/order_repository_impl.dart @@ -0,0 +1,76 @@ +import 'package:dartz/dartz.dart'; +import 'package:eco_bites/core/error/exceptions.dart'; +import 'package:eco_bites/core/error/failures.dart'; +import 'package:eco_bites/core/network/network_info.dart'; +import 'package:eco_bites/features/orders/data/datasources/order_local_data_source.dart'; +import 'package:eco_bites/features/orders/data/datasources/order_remote_data_source.dart'; +import 'package:eco_bites/features/orders/data/models/order_model.dart'; +import 'package:eco_bites/features/orders/domain/entities/order.dart' + as order_entity; +import 'package:eco_bites/features/orders/domain/repositories/order_repository.dart'; +import 'package:logger/logger.dart'; + +class OrderRepositoryImpl implements OrderRepository { + const OrderRepositoryImpl({ + required this.remoteDataSource, + required this.localDataSource, + required this.networkInfo, + }); + + final OrderRemoteDataSource remoteDataSource; + final OrderLocalDataSource localDataSource; + final NetworkInfo networkInfo; + + @override + Future>> getOrders() async { + if (await networkInfo.isConnected) { + try { + final List orders = await remoteDataSource.getOrders(); + await localDataSource.cacheOrders(orders); + return Right>(orders); + } on AuthException catch (e) { + Logger() + .w('Failed to fetch from remote, attempting to use cached data'); + try { + final List cachedOrders = + await localDataSource.getCachedOrders(); + return Right>(cachedOrders); + } on CacheException catch (_) { + return Left>( + AuthFailure(e.message), + ); + } + } + } else { + try { + final List cachedOrders = + await localDataSource.getCachedOrders(); + return Right>(cachedOrders); + } on CacheException { + return const Left>( + NetworkFailure('No internet connection'), + ); + } + } + } + + @override + Future> updateOrderStatus( + String orderId, + order_entity.OrderStatus newStatus, + ) async { + if (await networkInfo.isConnected) { + try { + await remoteDataSource.updateOrderStatus(orderId, newStatus); + await localDataSource.updateOrderStatus(orderId, newStatus); + 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/entities/order.dart b/lib/features/orders/domain/entities/order.dart new file mode 100644 index 0000000..69ecd59 --- /dev/null +++ b/lib/features/orders/domain/entities/order.dart @@ -0,0 +1,75 @@ +import 'package:equatable/equatable.dart'; + +enum OrderStatus { + pending, + confirmed, + ready, + completed, + cancelled; + + String get displayName { + switch (this) { + case OrderStatus.pending: + return 'Pending'; + case OrderStatus.confirmed: + return 'Confirmed'; + case OrderStatus.ready: + return 'Ready'; + case OrderStatus.completed: + return 'Completed'; + case OrderStatus.cancelled: + return 'Cancelled'; + } + } +} + +class Order extends Equatable { + const Order({ + required this.id, + required this.businessId, + required this.businessName, + required this.items, + required this.totalAmount, + required this.status, + required this.createdAt, + this.completedAt, + }); + + final String id; + final String businessId; + final String businessName; + final List items; + final double totalAmount; + final OrderStatus status; + final DateTime createdAt; + final DateTime? completedAt; + + @override + List get props => [ + id, + businessId, + businessName, + items, + totalAmount, + status, + createdAt, + completedAt, + ]; +} + +class OrderItem extends Equatable { + const OrderItem({ + required this.id, + required this.name, + required this.quantity, + required this.price, + }); + + final String id; + final String name; + final int quantity; + final double price; + + @override + List get props => [id, name, quantity, price]; +} diff --git a/lib/features/orders/domain/repositories/order_repository.dart b/lib/features/orders/domain/repositories/order_repository.dart new file mode 100644 index 0000000..a699cfc --- /dev/null +++ b/lib/features/orders/domain/repositories/order_repository.dart @@ -0,0 +1,12 @@ +import 'package:dartz/dartz.dart'; +import 'package:eco_bites/core/error/failures.dart'; +import 'package:eco_bites/features/orders/domain/entities/order.dart' + as entities; + +abstract class OrderRepository { + Future>> getOrders(); + Future> updateOrderStatus( + String orderId, + entities.OrderStatus newStatus, + ); +} diff --git a/lib/features/orders/domain/usecases/get_orders.dart b/lib/features/orders/domain/usecases/get_orders.dart new file mode 100644 index 0000000..625f6b5 --- /dev/null +++ b/lib/features/orders/domain/usecases/get_orders.dart @@ -0,0 +1,15 @@ +import 'package:dartz/dartz.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/repositories/order_repository.dart'; + +class GetOrders { + const GetOrders(this.repository); + + final OrderRepository repository; + + Future>> call() async { + return repository.getOrders(); + } +} diff --git a/lib/features/orders/domain/usecases/update_order_status.dart b/lib/features/orders/domain/usecases/update_order_status.dart new file mode 100644 index 0000000..815bce2 --- /dev/null +++ b/lib/features/orders/domain/usecases/update_order_status.dart @@ -0,0 +1,28 @@ +import 'package:dartz/dartz.dart'; +import 'package:eco_bites/core/error/failures.dart'; +import 'package:eco_bites/features/orders/domain/entities/order.dart'; +import 'package:eco_bites/features/orders/domain/repositories/order_repository.dart'; +import 'package:equatable/equatable.dart'; + +class UpdateOrderStatus { + const UpdateOrderStatus(this.repository); + + final OrderRepository repository; + + Future> call(UpdateOrderStatusParams params) async { + return repository.updateOrderStatus(params.orderId, params.newStatus); + } +} + +class UpdateOrderStatusParams extends Equatable { + const UpdateOrderStatusParams({ + required this.orderId, + required this.newStatus, + }); + + final String orderId; + final OrderStatus newStatus; + + @override + List get props => [orderId, newStatus]; +} diff --git a/lib/features/orders/presentation/bloc/order_bloc.dart b/lib/features/orders/presentation/bloc/order_bloc.dart index 6f2f4c0..9c3040c 100644 --- a/lib/features/orders/presentation/bloc/order_bloc.dart +++ b/lib/features/orders/presentation/bloc/order_bloc.dart @@ -1,34 +1,73 @@ +import 'package:dartz/dartz.dart'; import 'package:eco_bites/core/blocs/resettable_mixin.dart'; -import 'package:eco_bites/features/orders/domain/models/order.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/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'; import 'package:eco_bites/features/orders/presentation/bloc/order_state.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:logger/logger.dart'; class OrderBloc extends Bloc with ResettableMixin { - OrderBloc() : super(OrdersLoading()) { + OrderBloc({ + required GetOrders getOrders, + required UpdateOrderStatus updateOrderStatus, + }) : _getOrders = getOrders, + _updateOrderStatus = updateOrderStatus, + super(OrdersLoading()) { on(_onLoadOrders); + on(_onUpdateOrder); } - void _onLoadOrders(LoadOrders event, Emitter emit) { + final GetOrders _getOrders; + final UpdateOrderStatus _updateOrderStatus; + + Future _onLoadOrders(LoadOrders event, Emitter emit) async { emit(OrdersLoading()); - /// JUST FOR TESTING, DO NOT HARDCODE DATA LIKE THIS. - - final List orders = [ - Order(id: '1', title: 'Order 1', date: DateTime(2024, 9, 15)), - Order(id: '2', title: 'Order 2', date: DateTime(2024, 8, 20)), - Order(id: '3', title: 'Order 3', date: DateTime(2024, 7, 15)), - Order(id: '4', title: 'Order 4', date: DateTime(2024, 6, 20)), - Order(id: '5', title: 'Order 5', date: DateTime(2024, 5, 12)), - Order(id: '6', title: 'Order 6', date: DateTime(2024, 4, 23)), - Order(id: '7', title: 'Order 7', date: DateTime(2024, 3, 15)), - Order(id: '8', title: 'Order 8', date: DateTime(2024, 2, 20)), - Order(id: '9', title: 'Order 9', date: DateTime(2024, 1, 15)), - Order(id: '10', title: 'Order 10', date: DateTime(2023, 12, 20)), - ]; - - emit(OrdersLoaded(orders)); + final Either> result = await _getOrders(); + + result.fold( + (Failure failure) { + Logger().e('Failed to load orders: ${failure.message}'); + emit(OrdersError(failure.message)); + }, + (List orders) { + Logger().d('Loaded ${orders.length} orders'); + emit(OrdersLoaded(orders)); + }, + ); + } + + Future _onUpdateOrder( + UpdateOrder event, + Emitter emit, + ) async { + final OrderState currentState = state; + if (currentState is OrdersLoaded) { + emit(OrdersLoading()); + + final Either result = await _updateOrderStatus( + UpdateOrderStatusParams( + orderId: event.orderId, + newStatus: event.newStatus, + ), + ); + + result.fold( + (Failure failure) { + Logger().e('Failed to update order: ${failure.message}'); + emit(OrdersError(failure.message)); + }, + (_) async { + // Reload orders after successful update + add(LoadOrders()); + }, + ); + } } @override diff --git a/lib/features/orders/presentation/bloc/order_event.dart b/lib/features/orders/presentation/bloc/order_event.dart index 53daae6..da5e72a 100644 --- a/lib/features/orders/presentation/bloc/order_event.dart +++ b/lib/features/orders/presentation/bloc/order_event.dart @@ -1,9 +1,24 @@ +import 'package:eco_bites/features/orders/domain/entities/order.dart'; import 'package:equatable/equatable.dart'; + abstract class OrderEvent extends Equatable { + const OrderEvent(); + @override - List get props; + List get props => []; } -class LoadOrders extends OrderEvent { + +class LoadOrders extends OrderEvent {} + +class UpdateOrder extends OrderEvent { + const UpdateOrder({ + required this.orderId, + required this.newStatus, + }); + + final String orderId; + final OrderStatus newStatus; + @override - List get props => []; + List get props => [orderId, newStatus]; } diff --git a/lib/features/orders/presentation/bloc/order_state.dart b/lib/features/orders/presentation/bloc/order_state.dart index 090b5ac..48447ae 100644 --- a/lib/features/orders/presentation/bloc/order_state.dart +++ b/lib/features/orders/presentation/bloc/order_state.dart @@ -1,16 +1,31 @@ -import 'package:eco_bites/features/orders/domain/models/order.dart'; +import 'package:eco_bites/features/orders/domain/entities/order.dart'; import 'package:equatable/equatable.dart'; + abstract class OrderState extends Equatable { + const OrderState(); + @override List get props => []; } +class OrdersInitial extends OrderState {} + class OrdersLoading extends OrderState {} + class OrdersLoaded extends OrderState { - OrdersLoaded(List? orders) : orders = orders ?? []; + const OrdersLoaded(this.orders); final List orders; @override List get props => [orders]; } + +class OrdersError extends OrderState { + const OrdersError(this.message); + + final String message; + + @override + List get props => [message]; +} diff --git a/lib/features/orders/presentation/screens/order_details_screen.dart b/lib/features/orders/presentation/screens/order_details_screen.dart new file mode 100644 index 0000000..3b8b443 --- /dev/null +++ b/lib/features/orders/presentation/screens/order_details_screen.dart @@ -0,0 +1,126 @@ +import 'package:eco_bites/core/ui/widgets/custom_appbar.dart'; +import 'package:eco_bites/core/utils/date_utils.dart' as date_utils; +import 'package:eco_bites/features/orders/domain/entities/order.dart' + as order_entity; +import 'package:flutter/material.dart'; + +class OrderDetailsScreen extends StatelessWidget { + const OrderDetailsScreen({ + super.key, + required this.order, + }); + + final order_entity.Order order; + + @override + Widget build(BuildContext context) { + final ThemeData theme = Theme.of(context); + + return Scaffold( + appBar: CustomAppBar( + title: 'Order #${order.id.substring(0, 8)}', + showBackButton: true, + ), + body: ListView( + padding: const EdgeInsets.all(16), + children: [ + // Business Info + Text( + order.businessName, + style: theme.textTheme.headlineSmall, + ), + const SizedBox(height: 24), + + // Order Status + Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 6, + ), + decoration: BoxDecoration( + color: _getStatusColor(order.status).withOpacity(0.1), + borderRadius: BorderRadius.circular(16), + ), + child: Text( + order.status.displayName, + style: theme.textTheme.titleMedium?.copyWith( + color: _getStatusColor(order.status), + fontWeight: FontWeight.w500, + ), + ), + ), + const SizedBox(height: 24), + + // Order Items + Text( + 'Order Items', + style: theme.textTheme.titleLarge, + ), + const SizedBox(height: 16), + ...order.items.map( + (order_entity.OrderItem item) => Padding( + padding: const EdgeInsets.only(bottom: 12.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + '${item.quantity}x ${item.name}', + style: theme.textTheme.bodyLarge, + ), + ), + Text( + 'COP ${item.price.toStringAsFixed(2)}', + style: theme.textTheme.bodyLarge, + ), + ], + ), + ), + ), + const Divider(height: 32), + + // Total + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Total', + style: theme.textTheme.titleLarge, + ), + Text( + 'COP ${order.totalAmount.toStringAsFixed(2)}', + style: theme.textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + ], + ), + const SizedBox(height: 24), + + // Order Date + Text( + 'Ordered on ${date_utils.DateUtils.formatDateTime(order.createdAt)}', + style: theme.textTheme.bodyMedium?.copyWith( + color: theme.textTheme.bodyMedium?.color?.withOpacity(0.7), + ), + ), + ], + ), + ); + } + + Color _getStatusColor(order_entity.OrderStatus status) { + switch (status) { + case order_entity.OrderStatus.pending: + return Colors.orange; + case order_entity.OrderStatus.confirmed: + return Colors.blue; + case order_entity.OrderStatus.ready: + return Colors.green; + case order_entity.OrderStatus.completed: + return Colors.purple; + case order_entity.OrderStatus.cancelled: + return Colors.red; + } + } +} diff --git a/lib/features/orders/presentation/screens/order_list_screen.dart b/lib/features/orders/presentation/screens/order_list_screen.dart index 8ba4d47..03d6f53 100644 --- a/lib/features/orders/presentation/screens/order_list_screen.dart +++ b/lib/features/orders/presentation/screens/order_list_screen.dart @@ -1,7 +1,10 @@ import 'package:eco_bites/core/ui/widgets/custom_appbar.dart'; -import 'package:eco_bites/features/orders/domain/models/order.dart'; +import 'package:eco_bites/features/orders/domain/entities/order.dart' + as order_entity; import 'package:eco_bites/features/orders/presentation/bloc/order_bloc.dart'; +import 'package:eco_bites/features/orders/presentation/bloc/order_event.dart'; import 'package:eco_bites/features/orders/presentation/bloc/order_state.dart'; +import 'package:eco_bites/features/orders/presentation/screens/order_details_screen.dart'; import 'package:eco_bites/features/orders/presentation/widgets/order_item.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -20,6 +23,8 @@ class OrderListScreenContent extends StatelessWidget { @override Widget build(BuildContext context) { + context.read().add(LoadOrders()); + return Scaffold( appBar: const CustomAppBar(title: 'Orders'), body: BlocBuilder( @@ -33,8 +38,19 @@ class OrderListScreenContent extends StatelessWidget { return ListView.builder( itemCount: state.orders.length, itemBuilder: (BuildContext context, int index) { - final Order order = state.orders[index]; - return OrderItem(order: order); + final order_entity.Order order = state.orders[index]; + return OrderItem( + order: order, + onTap: () { + Navigator.of(context).push( + MaterialPageRoute( + builder: (BuildContext context) => OrderDetailsScreen( + order: order, + ), + ), + ); + }, + ); }, ); } else { diff --git a/lib/features/orders/presentation/widgets/order_item.dart b/lib/features/orders/presentation/widgets/order_item.dart index 518b6cf..2e6cb2e 100644 --- a/lib/features/orders/presentation/widgets/order_item.dart +++ b/lib/features/orders/presentation/widgets/order_item.dart @@ -1,59 +1,25 @@ -import 'package:eco_bites/core/ui/widgets/basic_image.dart'; import 'package:eco_bites/core/utils/date_utils.dart' as date_utils; -import 'package:eco_bites/features/orders/domain/models/order.dart'; -import 'package:eco_bites/features/orders/presentation/bloc/order_item_bloc.dart'; -import 'package:eco_bites/features/orders/presentation/bloc/order_item_state.dart'; +import 'package:eco_bites/features/orders/domain/entities/order.dart' + as order_entity; import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; const double cardBorderRadius = 24.0; const EdgeInsets cardMargin = EdgeInsets.symmetric(vertical: 8, horizontal: 16); const EdgeInsets cardPadding = EdgeInsets.all(16); -const double spaceBetweenElements = 16.0; +const double spaceBetweenElements = 12.0; class OrderItem extends StatelessWidget { const OrderItem({ super.key, required this.order, - this.status = 'Delivered', + required this.onTap, }); - final Order order; - final String status; - - // Helper method to map the enum value to a user-friendly text. - String _mapStatusToText(OrderItemStatus status) { - switch (status) { - case OrderItemStatus.pending: - return 'Pending'; - case OrderItemStatus.processing: - return 'Processing'; - case OrderItemStatus.shipped: - return 'Shipped'; - case OrderItemStatus.delivered: - return 'Delivered'; - case OrderItemStatus.cancelled: - return 'Cancelled'; - } - } - - // Helper method to convert String to OrderItemStatus - OrderItemStatus _mapStatus(String status) { - return OrderItemStatus.values.firstWhere( - (OrderItemStatus e) => e.toString().split('.').last == status.toLowerCase(), - orElse: () => OrderItemStatus.delivered, // Default to delivered if not found - ); - } + final order_entity.Order order; + final VoidCallback onTap; @override Widget build(BuildContext context) { - return BlocProvider( - create: (BuildContext context) => OrderItemBloc(_mapStatus(status)), // Convert String to OrderItemStatus - child: _buildOrderItem(context), - ); - } - - Widget _buildOrderItem(BuildContext context) { final ThemeData theme = Theme.of(context); return Card( @@ -61,59 +27,73 @@ class OrderItem extends StatelessWidget { borderRadius: BorderRadius.circular(cardBorderRadius), ), margin: cardMargin, - child: Padding( - padding: cardPadding, - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - BasicImage(imageUrl: order.imageUrl), - const SizedBox(width: spaceBetweenElements), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + child: InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(cardBorderRadius), + child: Padding( + padding: cardPadding, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + // Business Name + Text( + order.businessName, + style: theme.textTheme.titleLarge, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: spaceBetweenElements), + + // Status and Date + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: Text( - order.title, - style: theme.textTheme.titleLarge, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 6, + ), + decoration: BoxDecoration( + color: _getStatusColor(order.status).withOpacity(0.1), + borderRadius: BorderRadius.circular(16), + ), + child: Text( + order.status.displayName, + style: theme.textTheme.bodyMedium?.copyWith( + color: _getStatusColor(order.status), + fontWeight: FontWeight.w500, ), - ], + ), ), - const SizedBox(height: spaceBetweenElements), - BlocBuilder( - builder: (BuildContext context, OrderItemState state) { - final String statusText = _mapStatusToText(state.status); - - return Row( - children: [ - // Status text - Text( - statusText, - style: theme.textTheme.bodyMedium, - ), - // Add some space between the status and date - const SizedBox(width: 10), - // Date text aligned next to the status - Text( - date_utils.DateUtils.formatDayMonth(order.date), - style: theme.textTheme.bodyMedium, - ), - ], - ); - }, + Text( + date_utils.DateUtils.formatDayMonth(order.createdAt), + style: theme.textTheme.bodyMedium?.copyWith( + color: + theme.textTheme.bodyMedium?.color?.withOpacity(0.7), + ), ), ], ), - ), - ], + ], + ), ), ), ); } + + Color _getStatusColor(order_entity.OrderStatus status) { + switch (status) { + case order_entity.OrderStatus.pending: + return Colors.orange; + case order_entity.OrderStatus.confirmed: + return Colors.blue; + case order_entity.OrderStatus.ready: + return Colors.green; + case order_entity.OrderStatus.completed: + return Colors.purple; + case order_entity.OrderStatus.cancelled: + return Colors.red; + } + } } diff --git a/lib/injection_container.dart b/lib/injection_container.dart index 504ea2f..e6aed60 100644 --- a/lib/injection_container.dart +++ b/lib/injection_container.dart @@ -27,6 +27,12 @@ import 'package:eco_bites/features/food/data/repositories/food_business_reposito import 'package:eco_bites/features/food/domain/repositories/food_business_repository.dart'; import 'package:eco_bites/features/food/domain/usecases/fetch_nearby_surplus_food_businesses.dart'; import 'package:eco_bites/features/food/presentation/bloc/food_business_bloc.dart'; +import 'package:eco_bites/features/orders/data/datasources/order_local_data_source.dart'; +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/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'; import 'package:eco_bites/features/profile/data/repositories/profile_repository_impl.dart'; import 'package:eco_bites/features/profile/domain/repositories/user_profile_repository.dart'; @@ -177,7 +183,37 @@ void _setupAddressFeature() { } void _setupOrderFeature() { - serviceLocator.registerFactory(() => OrderBloc()); + serviceLocator.registerFactory( + () => OrderBloc( + getOrders: serviceLocator(), + updateOrderStatus: serviceLocator(), + ), + ); + + // Use cases + serviceLocator.registerLazySingleton(() => GetOrders(serviceLocator())); + serviceLocator + .registerLazySingleton(() => UpdateOrderStatus(serviceLocator())); + + // Repository + serviceLocator.registerLazySingleton( + () => OrderRepositoryImpl( + remoteDataSource: serviceLocator(), + localDataSource: serviceLocator(), + networkInfo: serviceLocator(), + ), + ); + + // Data sources + serviceLocator.registerLazySingleton( + () => OrderRemoteDataSourceImpl( + firestore: serviceLocator(), + auth: serviceLocator(), + ), + ); + serviceLocator.registerLazySingleton( + () => OrderLocalDataSourceImpl(), + ); } void _setupCartFeature() {