From fc6a4f038e72d217559b59ab792b493257f61761 Mon Sep 17 00:00:00 2001 From: Phuoc Nguyen Date: Thu, 11 Dec 2025 16:39:25 +0700 Subject: [PATCH] fix cart, fix log cart --- lib/core/services/sentry_service.dart | 3 +- .../cart/presentation/pages/cart_page.dart | 33 +++++----- .../widgets/cart_item_widget.dart | 63 ++++++++++--------- .../products_remote_datasource.dart | 10 ++- 4 files changed, 65 insertions(+), 44 deletions(-) diff --git a/lib/core/services/sentry_service.dart b/lib/core/services/sentry_service.dart index 04e62e1..3db1b43 100644 --- a/lib/core/services/sentry_service.dart +++ b/lib/core/services/sentry_service.dart @@ -79,7 +79,8 @@ class SentryService { ..beforeSend = (event, hint) { // Filter out certain errors if needed // Return null to drop the event - return event; + // return event; + return null; }; }, appRunner: appRunner, diff --git a/lib/features/cart/presentation/pages/cart_page.dart b/lib/features/cart/presentation/pages/cart_page.dart index 2f9ce90..2d1cac4 100644 --- a/lib/features/cart/presentation/pages/cart_page.dart +++ b/lib/features/cart/presentation/pages/cart_page.dart @@ -36,6 +36,7 @@ class CartPage extends ConsumerStatefulWidget { class _CartPageState extends ConsumerState { bool _isSyncing = false; + bool _hasLoggedViewCart = false; // Cart is initialized once in home_page.dart at app startup // Provider has keepAlive: true, so no need to reload here @@ -44,29 +45,32 @@ class _CartPageState extends ConsumerState { // and in checkout button handler for checkout flow. // No dispose() method needed - using ref.read() in dispose() is unsafe. + void _logViewCartOnce(CartState cartState) { + if (_hasLoggedViewCart || cartState.isEmpty) return; + _hasLoggedViewCart = true; + + AnalyticsService.logViewCart( + cartValue: cartState.totalPrice, + items: cartState.items.map((item) => AnalyticsEventItem( + itemId: item.product.productId, + itemName: item.product.name, + price: item.product.basePrice, + quantity: item.quantity.toInt(), + )).toList(), + ); + } + @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; final cartState = ref.watch(cartProvider); - + // Log view cart analytics event only once when page opens + _logViewCartOnce(cartState); final itemCount = cartState.itemCount; final hasSelection = cartState.selectedCount > 0; - // Log view cart analytics event when cart has items - if (cartState.isNotEmpty) { - AnalyticsService.logViewCart( - cartValue: cartState.selectedTotal, - items: cartState.items.map((item) => AnalyticsEventItem( - itemId: item.product.productId, - itemName: item.product.name, - price: item.product.basePrice, - quantity: item.quantity.toInt(), - )).toList(), - ); - } - return PopScope( // Intercept back navigation to sync pending updates onPopInvokedWithResult: (didPop, result) async { @@ -346,6 +350,7 @@ class _CartPageState extends ConsumerState { /// Build error banner (shown at top when there's an error but cart has items) Widget _buildErrorBanner(String errorMessage) { + print(errorMessage); return Container( width: double.infinity, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), diff --git a/lib/features/cart/presentation/widgets/cart_item_widget.dart b/lib/features/cart/presentation/widgets/cart_item_widget.dart index e82ee19..753a543 100644 --- a/lib/features/cart/presentation/widgets/cart_item_widget.dart +++ b/lib/features/cart/presentation/widgets/cart_item_widget.dart @@ -7,7 +7,7 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; -import 'package:intl/intl.dart'; +import 'package:go_router/go_router.dart'; import 'package:worker/core/utils/extensions.dart'; import 'package:worker/core/widgets/loading_indicator.dart'; import 'package:worker/core/theme/typography.dart'; @@ -113,34 +113,40 @@ class _CartItemWidgetState extends ConsumerState { const SizedBox(width: 12), - // Product Image (bigger: 100x100) - ClipRRect( - borderRadius: BorderRadius.circular(8), - child: CachedNetworkImage( - imageUrl: widget.item.product.thumbnail.isNotEmpty - ? widget.item.product.thumbnail - : (widget.item.product.images.isNotEmpty - ? widget.item.product.images.first - : ''), - width: 100, - height: 100, - fit: BoxFit.cover, - placeholder: (context, url) => Container( + // Product Image (bigger: 100x100) - tap to navigate to product detail + GestureDetector( + onTap: () { + // Navigate to product detail with product ID in path + context.push('/products/${widget.item.product.productId}'); + }, + child: ClipRRect( + borderRadius: BorderRadius.circular(8), + child: CachedNetworkImage( + imageUrl: widget.item.product.thumbnail.isNotEmpty + ? widget.item.product.thumbnail + : (widget.item.product.images.isNotEmpty + ? widget.item.product.images.first + : ''), width: 100, height: 100, - color: colorScheme.surfaceContainerHighest, - child: Center( - child: CustomLoadingIndicator(color: colorScheme.primary, size: 20), + fit: BoxFit.cover, + placeholder: (context, url) => Container( + width: 100, + height: 100, + color: colorScheme.surfaceContainerHighest, + child: Center( + child: CustomLoadingIndicator(color: colorScheme.primary, size: 20), + ), ), - ), - errorWidget: (context, url, error) => Container( - width: 100, - height: 100, - color: colorScheme.surfaceContainerHighest, - child: FaIcon( - FontAwesomeIcons.image, - color: colorScheme.onSurfaceVariant, - size: 32, + errorWidget: (context, url, error) => Container( + width: 100, + height: 100, + color: colorScheme.surfaceContainerHighest, + child: FaIcon( + FontAwesomeIcons.image, + color: colorScheme.onSurfaceVariant, + size: 32, + ), ), ), ), @@ -193,14 +199,15 @@ class _CartItemWidgetState extends ConsumerState { const SizedBox(width: 8), - // Quantity TextField + // Quantity TextField - uses text keyboard for Done button on iOS/Android SizedBox( width: 50, height: 32, child: TextField( controller: _quantityController, focusNode: _quantityFocusNode, - keyboardType: TextInputType.number, + keyboardType: TextInputType.text, + textInputAction: TextInputAction.done, textAlign: TextAlign.center, style: AppTypography.titleMedium.copyWith( fontWeight: FontWeight.w600, diff --git a/lib/features/products/data/datasources/products_remote_datasource.dart b/lib/features/products/data/datasources/products_remote_datasource.dart index 4a06121..66683ed 100644 --- a/lib/features/products/data/datasources/products_remote_datasource.dart +++ b/lib/features/products/data/datasources/products_remote_datasource.dart @@ -138,7 +138,15 @@ class ProductsRemoteDataSource { throw Exception('Product not found: $itemCode'); } - return ProductModel.fromJson(message as Map); + // Handle API error response: {success: false, message: "Item not found"} + if (message is Map) { + if (message['success'] == false) { + throw Exception(message['message'] ?? 'Product not found: $itemCode'); + } + return ProductModel.fromJson(message); + } + + throw Exception('Invalid response format for product: $itemCode'); } on DioException catch (e) { if (e.response?.statusCode == 404) { throw Exception('Product not found: $itemCode');