fix cart, fix log cart

This commit is contained in:
Phuoc Nguyen
2025-12-11 16:39:25 +07:00
parent e3632d4445
commit fc6a4f038e
4 changed files with 65 additions and 44 deletions

View File

@@ -79,7 +79,8 @@ class SentryService {
..beforeSend = (event, hint) { ..beforeSend = (event, hint) {
// Filter out certain errors if needed // Filter out certain errors if needed
// Return null to drop the event // Return null to drop the event
return event; // return event;
return null;
}; };
}, },
appRunner: appRunner, appRunner: appRunner,

View File

@@ -36,6 +36,7 @@ class CartPage extends ConsumerStatefulWidget {
class _CartPageState extends ConsumerState<CartPage> { class _CartPageState extends ConsumerState<CartPage> {
bool _isSyncing = false; bool _isSyncing = false;
bool _hasLoggedViewCart = false;
// Cart is initialized once in home_page.dart at app startup // Cart is initialized once in home_page.dart at app startup
// Provider has keepAlive: true, so no need to reload here // Provider has keepAlive: true, so no need to reload here
@@ -44,29 +45,32 @@ class _CartPageState extends ConsumerState<CartPage> {
// and in checkout button handler for checkout flow. // and in checkout button handler for checkout flow.
// No dispose() method needed - using ref.read() in dispose() is unsafe. // 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme; final colorScheme = Theme.of(context).colorScheme;
final cartState = ref.watch(cartProvider); final cartState = ref.watch(cartProvider);
// Log view cart analytics event only once when page opens
_logViewCartOnce(cartState);
final itemCount = cartState.itemCount; final itemCount = cartState.itemCount;
final hasSelection = cartState.selectedCount > 0; 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( return PopScope(
// Intercept back navigation to sync pending updates // Intercept back navigation to sync pending updates
onPopInvokedWithResult: (didPop, result) async { onPopInvokedWithResult: (didPop, result) async {
@@ -346,6 +350,7 @@ class _CartPageState extends ConsumerState<CartPage> {
/// Build error banner (shown at top when there's an error but cart has items) /// Build error banner (shown at top when there's an error but cart has items)
Widget _buildErrorBanner(String errorMessage) { Widget _buildErrorBanner(String errorMessage) {
print(errorMessage);
return Container( return Container(
width: double.infinity, width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),

View File

@@ -7,7 +7,7 @@ import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.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/utils/extensions.dart';
import 'package:worker/core/widgets/loading_indicator.dart'; import 'package:worker/core/widgets/loading_indicator.dart';
import 'package:worker/core/theme/typography.dart'; import 'package:worker/core/theme/typography.dart';
@@ -113,34 +113,40 @@ class _CartItemWidgetState extends ConsumerState<CartItemWidget> {
const SizedBox(width: 12), const SizedBox(width: 12),
// Product Image (bigger: 100x100) // Product Image (bigger: 100x100) - tap to navigate to product detail
ClipRRect( GestureDetector(
borderRadius: BorderRadius.circular(8), onTap: () {
child: CachedNetworkImage( // Navigate to product detail with product ID in path
imageUrl: widget.item.product.thumbnail.isNotEmpty context.push('/products/${widget.item.product.productId}');
? widget.item.product.thumbnail },
: (widget.item.product.images.isNotEmpty child: ClipRRect(
? widget.item.product.images.first borderRadius: BorderRadius.circular(8),
: ''), child: CachedNetworkImage(
width: 100, imageUrl: widget.item.product.thumbnail.isNotEmpty
height: 100, ? widget.item.product.thumbnail
fit: BoxFit.cover, : (widget.item.product.images.isNotEmpty
placeholder: (context, url) => Container( ? widget.item.product.images.first
: ''),
width: 100, width: 100,
height: 100, height: 100,
color: colorScheme.surfaceContainerHighest, fit: BoxFit.cover,
child: Center( placeholder: (context, url) => Container(
child: CustomLoadingIndicator(color: colorScheme.primary, size: 20), width: 100,
height: 100,
color: colorScheme.surfaceContainerHighest,
child: Center(
child: CustomLoadingIndicator(color: colorScheme.primary, size: 20),
),
), ),
), errorWidget: (context, url, error) => Container(
errorWidget: (context, url, error) => Container( width: 100,
width: 100, height: 100,
height: 100, color: colorScheme.surfaceContainerHighest,
color: colorScheme.surfaceContainerHighest, child: FaIcon(
child: FaIcon( FontAwesomeIcons.image,
FontAwesomeIcons.image, color: colorScheme.onSurfaceVariant,
color: colorScheme.onSurfaceVariant, size: 32,
size: 32, ),
), ),
), ),
), ),
@@ -193,14 +199,15 @@ class _CartItemWidgetState extends ConsumerState<CartItemWidget> {
const SizedBox(width: 8), const SizedBox(width: 8),
// Quantity TextField // Quantity TextField - uses text keyboard for Done button on iOS/Android
SizedBox( SizedBox(
width: 50, width: 50,
height: 32, height: 32,
child: TextField( child: TextField(
controller: _quantityController, controller: _quantityController,
focusNode: _quantityFocusNode, focusNode: _quantityFocusNode,
keyboardType: TextInputType.number, keyboardType: TextInputType.text,
textInputAction: TextInputAction.done,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: AppTypography.titleMedium.copyWith( style: AppTypography.titleMedium.copyWith(
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,

View File

@@ -138,7 +138,15 @@ class ProductsRemoteDataSource {
throw Exception('Product not found: $itemCode'); throw Exception('Product not found: $itemCode');
} }
return ProductModel.fromJson(message as Map<String, dynamic>); // Handle API error response: {success: false, message: "Item not found"}
if (message is Map<String, dynamic>) {
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) { } on DioException catch (e) {
if (e.response?.statusCode == 404) { if (e.response?.statusCode == 404) {
throw Exception('Product not found: $itemCode'); throw Exception('Product not found: $itemCode');