From 687d980e80e2436f3c7e9776c2320e135d610a7a Mon Sep 17 00:00:00 2001 From: Abel Arismendy Date: Mon, 2 Dec 2024 22:20:11 -0500 Subject: [PATCH] analytics events --- lib/core/ui/layouts/main_layout.dart | 20 ++++- lib/core/utils/analytics_logger.dart | 35 ++++++++ .../cart/presentation/bloc/cart_bloc.dart | 83 ++++++++++++++----- .../presentation/screens/home_screen.dart | 22 +++++ .../screens/order_details_screen.dart | 56 +++++++++++++ .../presentation/screens/splash_screen.dart | 27 ++---- .../presentation/widgets/ticket_form.dart | 28 ++++++- 7 files changed, 224 insertions(+), 47 deletions(-) create mode 100644 lib/core/utils/analytics_logger.dart diff --git a/lib/core/ui/layouts/main_layout.dart b/lib/core/ui/layouts/main_layout.dart index 180e13d..a7513ec 100644 --- a/lib/core/ui/layouts/main_layout.dart +++ b/lib/core/ui/layouts/main_layout.dart @@ -1,4 +1,5 @@ import 'package:eco_bites/core/ui/widgets/bottom_navbar.dart'; +import 'package:eco_bites/core/utils/analytics_logger.dart'; import 'package:eco_bites/features/cart/presentation/screens/cart_screen.dart'; import 'package:eco_bites/features/home/presentation/bloc/home_bloc.dart'; import 'package:eco_bites/features/home/presentation/screens/home_screen.dart'; @@ -9,8 +10,6 @@ import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class MainLayout extends StatefulWidget { - // Pass the app launch time here - const MainLayout({super.key, required this.appLaunchTime}); final DateTime appLaunchTime; @@ -32,7 +31,7 @@ class MainLayoutState extends State { create: (BuildContext context) => HomeBloc(), child: HomeScreen( appLaunchTime: widget.appLaunchTime, - ), // Pass appLaunchTime to HomeScreen + ), ), const CartScreen(), const OrderListScreen(), @@ -41,6 +40,21 @@ class MainLayoutState extends State { } void _onItemTapped(int index) { + final List screenNames = [ + 'home', + 'cart', + 'orders', + 'profile', + ]; + + AnalyticsLogger.logEvent( + eventName: 'navigation', + additionalData: { + 'screen': screenNames[index], + 'previous_screen': screenNames[_currentIndex], + }, + ); + setState(() { _currentIndex = index; }); diff --git a/lib/core/utils/analytics_logger.dart b/lib/core/utils/analytics_logger.dart new file mode 100644 index 0000000..a951573 --- /dev/null +++ b/lib/core/utils/analytics_logger.dart @@ -0,0 +1,35 @@ +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:firebase_auth/firebase_auth.dart'; + +class AnalyticsLogger { + const AnalyticsLogger._(); + + static Future logEvent({ + required String eventName, + Map? additionalData, + int? milliseconds, + bool? authenticated, + }) async { + try { + await FirebaseFirestore.instance.collection('logs').add({ + 'eventName': eventName, + 'timestamp': FieldValue.serverTimestamp(), + 'userId': await _getCurrentUserId(), + if (milliseconds != null) 'milliseconds': milliseconds, + if (authenticated != null) 'authenticated': authenticated, + if (additionalData != null) ...additionalData, + }); + } catch (e) { + // Silently fail if logging fails + } + } + + static Future _getCurrentUserId() async { + try { + final String? userId = FirebaseAuth.instance.currentUser?.uid; + return userId; + } catch (e) { + return null; + } + } +} diff --git a/lib/features/cart/presentation/bloc/cart_bloc.dart b/lib/features/cart/presentation/bloc/cart_bloc.dart index b3fa7ef..da730c6 100644 --- a/lib/features/cart/presentation/bloc/cart_bloc.dart +++ b/lib/features/cart/presentation/bloc/cart_bloc.dart @@ -1,4 +1,5 @@ import 'package:eco_bites/core/blocs/resettable_mixin.dart'; +import 'package:eco_bites/core/utils/analytics_logger.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'; @@ -18,21 +19,19 @@ class CartBloc extends Bloc on(_onCartItemQuantityChanged); on(_onCartItemRemoved); on(_onClearCart); - on(_onCompletePurchase); on(_onPurchaseCart); } final OrderBloc orderBloc; void _onClearCart(ClearCart event, Emitter emit) { - emit( - const CartState(items: []), - ); // Set items to an empty list - } - - void _onCompletePurchase(CompletePurchase event, Emitter emit) { - // Here you could add logic to process the purchase - // For now, it clears the cart to simulate a completed purchase + AnalyticsLogger.logEvent( + eventName: 'clear_cart', + additionalData: { + 'item_count': state.items.length, + 'total_amount': state.subtotal, + }, + ); emit(const CartState(items: [])); } @@ -45,23 +44,30 @@ class CartBloc extends Bloc existingItem = null; } + final List updatedItems = [...state.items]; if (existingItem != null) { - final List updatedItems = - state.items.map((CartItemData item) { - if (item.id == event.item.id) { - return item.copyWith(quantity: item.quantity + event.item.quantity); - } - return item; - }).toList(); - emit(CartState(items: updatedItems)); + final int index = updatedItems.indexOf(existingItem); + updatedItems[index] = existingItem.copyWith( + quantity: existingItem.quantity + 1, + ); } else { - final List updatedItems = - List.from(state.items)..add(event.item); - emit(CartState(items: updatedItems)); + updatedItems.add(event.item); } + + AnalyticsLogger.logEvent( + eventName: 'add_to_cart', + additionalData: { + 'item_id': event.item.id, + 'item_name': event.item.title, + 'business_id': event.item.businessId, + 'price': event.item.offerPrice, + 'is_new_item': existingItem == null, + }, + ); + + emit(CartState(items: updatedItems)); } - // Existing handlers (no changes needed for these) void _onCartItemQuantityChanged( CartItemQuantityChanged event, Emitter emit, @@ -73,10 +79,36 @@ class CartBloc extends Bloc } return item; }).toList(); + + AnalyticsLogger.logEvent( + eventName: 'update_cart_quantity', + additionalData: { + 'item_id': event.itemId, + 'new_quantity': event.quantity, + 'previous_quantity': state.items + .firstWhere((CartItemData item) => item.id == event.itemId) + .quantity, + }, + ); + emit(CartState(items: updatedItems)); } void _onCartItemRemoved(CartItemRemoved event, Emitter emit) { + final CartItemData removedItem = + state.items.firstWhere((CartItemData item) => item.id == event.itemId); + + AnalyticsLogger.logEvent( + eventName: 'remove_from_cart', + additionalData: { + 'item_id': event.itemId, + 'item_name': removedItem.title, + 'business_id': removedItem.businessId, + 'quantity': removedItem.quantity, + 'price': removedItem.offerPrice, + }, + ); + final List updatedItems = state.items .where((CartItemData item) => item.id != event.itemId) .toList(); @@ -118,6 +150,15 @@ class CartBloc extends Bloc createdAt: DateTime.now(), ); + AnalyticsLogger.logEvent( + eventName: 'create_order', + additionalData: { + 'business_id': order.businessId, + 'item_count': order.items.length, + 'total_amount': order.totalAmount, + }, + ); + orderBloc.add(CreateOrderEvent(order: order)); // Clear cart after successful purchase diff --git a/lib/features/home/presentation/screens/home_screen.dart b/lib/features/home/presentation/screens/home_screen.dart index c12d678..0820754 100644 --- a/lib/features/home/presentation/screens/home_screen.dart +++ b/lib/features/home/presentation/screens/home_screen.dart @@ -1,4 +1,6 @@ import 'dart:async'; + +import 'package:eco_bites/core/utils/analytics_logger.dart'; import 'package:eco_bites/core/utils/analytics_service.dart'; import 'package:eco_bites/core/utils/distance.dart'; import 'package:eco_bites/features/address/domain/entities/address.dart'; @@ -114,6 +116,13 @@ class _HomeScreenContentState extends State void _handleTabSelection() { if (_tabController.indexIsChanging) { context.read().add(TabChanged(_tabController.index)); + AnalyticsLogger.logEvent( + eventName: 'tab_change', + additionalData: { + 'tab': _getTabName(_tabController.index), + 'previous_tab': _getTabName(_tabController.previousIndex), + }, + ); } } @@ -341,4 +350,17 @@ class _HomeScreenContentState extends State ), ); } + + String _getTabName(int index) { + final List tabNames = [ + 'for_you', + 'dietary', + 'restaurant', + 'ingredients', + 'store', + 'diary', + 'drink', + ]; + return tabNames[index]; + } } diff --git a/lib/features/orders/presentation/screens/order_details_screen.dart b/lib/features/orders/presentation/screens/order_details_screen.dart index 3b8b443..009a528 100644 --- a/lib/features/orders/presentation/screens/order_details_screen.dart +++ b/lib/features/orders/presentation/screens/order_details_screen.dart @@ -1,8 +1,13 @@ import 'package:eco_bites/core/ui/widgets/custom_appbar.dart'; +import 'package:eco_bites/core/utils/analytics_logger.dart'; import 'package:eco_bites/core/utils/date_utils.dart' as date_utils; +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/presentation/bloc/order_bloc.dart'; +import 'package:eco_bites/features/orders/presentation/bloc/order_event.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; class OrderDetailsScreen extends StatelessWidget { const OrderDetailsScreen({ @@ -12,6 +17,44 @@ class OrderDetailsScreen extends StatelessWidget { final order_entity.Order order; + void _handleOrderAgain(BuildContext context) { + // Create a new order with the same items + final OrderModel newOrder = OrderModel( + id: '', // ID will be generated by Firestore + businessId: order.businessId, + businessName: order.businessName, + items: order.items, + totalAmount: order.totalAmount, + status: order_entity.OrderStatus.pending, + createdAt: DateTime.now(), + ); + + // Log the Order Again feature usage + AnalyticsLogger.logEvent( + eventName: 'order_again', + additionalData: { + 'businessId': order.businessId, + 'originalOrderId': order.id, + 'itemCount': order.items.length, + 'totalAmount': order.totalAmount, + }, + ); + + // Dispatch create order event + context.read().add(CreateOrderEvent(order: newOrder)); + + // Show a snackbar to indicate the order is being processed + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Creating new order...'), + duration: Duration(seconds: 2), + ), + ); + + // Navigate back to the orders list + Navigator.of(context).pop(); + } + @override Widget build(BuildContext context) { final ThemeData theme = Theme.of(context); @@ -104,6 +147,19 @@ class OrderDetailsScreen extends StatelessWidget { color: theme.textTheme.bodyMedium?.color?.withOpacity(0.7), ), ), + const SizedBox(height: 32), + + // Order Again Button + ElevatedButton( + onPressed: () => _handleOrderAgain(context), + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + ), + child: const Text('Order Again'), + ), ], ), ); diff --git a/lib/features/splash/presentation/screens/splash_screen.dart b/lib/features/splash/presentation/screens/splash_screen.dart index 2107d54..1292390 100644 --- a/lib/features/splash/presentation/screens/splash_screen.dart +++ b/lib/features/splash/presentation/screens/splash_screen.dart @@ -1,6 +1,6 @@ // ignore_for_file: use_build_context_synchronously -import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:eco_bites/core/network/network_info.dart'; +import 'package:eco_bites/core/utils/analytics_logger.dart'; import 'package:eco_bites/features/splash/presentation/bloc/splash_bloc.dart'; import 'package:eco_bites/features/splash/presentation/bloc/splash_event.dart'; import 'package:eco_bites/features/splash/presentation/bloc/splash_state.dart'; @@ -33,10 +33,10 @@ class SplashScreen extends StatelessWidget { if (state is Authenticated) { try { if (await networkInfo.isConnected) { - await logEvent( + await AnalyticsLogger.logEvent( + eventName: 'splash_screen', milliseconds: loadTime.inMilliseconds, authenticated: true, - eventName: 'splash_screen', ); } } catch (e) { @@ -46,10 +46,10 @@ class SplashScreen extends StatelessWidget { } else if (state is Unauthenticated) { try { if (await networkInfo.isConnected) { - await logEvent( + await AnalyticsLogger.logEvent( + eventName: 'splash_screen', milliseconds: loadTime.inMilliseconds, authenticated: false, - eventName: 'splash_screen', ); } } catch (e) { @@ -79,21 +79,4 @@ class SplashScreen extends StatelessWidget { ), ); } - - Future logEvent({ - required int milliseconds, - required bool authenticated, - required String eventName, - }) async { - try { - await FirebaseFirestore.instance.collection('logs').add({ - 'milliseconds': milliseconds, - 'authenticated': authenticated, - 'eventName': eventName, - 'timestamp': FieldValue.serverTimestamp(), - }); - } catch (e) { - // Silently fail if logging fails - } - } } diff --git a/lib/features/support/presentation/widgets/ticket_form.dart b/lib/features/support/presentation/widgets/ticket_form.dart index 31d48a3..5a5f53d 100644 --- a/lib/features/support/presentation/widgets/ticket_form.dart +++ b/lib/features/support/presentation/widgets/ticket_form.dart @@ -1,3 +1,4 @@ +import 'package:eco_bites/core/utils/analytics_logger.dart'; import 'package:eco_bites/features/support/presentation/bloc/support_bloc.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -15,21 +16,46 @@ class TicketForm extends StatelessWidget { return BlocConsumer( listener: (BuildContext context, SupportState state) { if (state is SupportSuccess) { + AnalyticsLogger.logEvent( + eventName: 'support_ticket_submitted', + additionalData: { + 'category': category, + 'sub_option': subOption, + 'reason_length': reasonController.text.length, + 'description_length': descriptionController.text.length, + }, + ); ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Ticket submitted successfully')), ); Navigator.pop(context); } else if (state is SupportFailure) { + AnalyticsLogger.logEvent( + eventName: 'support_ticket_failed', + additionalData: { + 'category': category, + 'sub_option': subOption, + 'error': state.error, + }, + ); ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Failed to submit ticket: ${state.error}')), ); } else if (state is SupportCached) { + AnalyticsLogger.logEvent( + eventName: 'support_ticket_cached', + additionalData: { + 'category': category, + 'sub_option': subOption, + 'message': state.message, + }, + ); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(state.message), ), ); - Navigator.pop(context); // Navegar hacia atrás al guardar en caché + Navigator.pop(context); } }, builder: (BuildContext context, SupportState state) {