update cart/favorite

This commit is contained in:
Phuoc Nguyen
2025-12-03 15:53:46 +07:00
parent e1c9f818d2
commit 27798cc234
19 changed files with 370 additions and 119 deletions

View File

@@ -3,7 +3,7 @@
/// Shopping cart screen with selection and checkout.
/// Features expanded item list with total price at bottom.
library;
import 'package:worker/core/utils/extensions.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
@@ -35,14 +35,8 @@ class CartPage extends ConsumerStatefulWidget {
class _CartPageState extends ConsumerState<CartPage> {
bool _isSyncing = false;
@override
void initState() {
super.initState();
// Initialize cart from API on mount
WidgetsBinding.instance.addPostFrameCallback((_) {
ref.read(cartProvider.notifier).initialize();
});
}
// Cart is initialized once in home_page.dart at app startup
// Provider has keepAlive: true, so no need to reload here
// Note: Sync is handled in PopScope.onPopInvokedWithResult for back navigation
// and in checkout button handler for checkout flow.
@@ -53,11 +47,7 @@ class _CartPageState extends ConsumerState<CartPage> {
final colorScheme = Theme.of(context).colorScheme;
final cartState = ref.watch(cartProvider);
final currencyFormatter = NumberFormat.currency(
locale: 'vi_VN',
symbol: 'đ',
decimalDigits: 0,
);
final itemCount = cartState.itemCount;
final hasSelection = cartState.selectedCount > 0;
@@ -144,7 +134,11 @@ class _CartPageState extends ConsumerState<CartPage> {
context,
cartState,
ref,
currencyFormatter,
NumberFormat.currency(
locale: 'vi_VN',
symbol: 'đ',
decimalDigits: 0,
),
hasSelection,
),
],

View File

@@ -9,7 +9,6 @@ import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:worker/features/cart/presentation/providers/cart_data_providers.dart';
import 'package:worker/features/cart/presentation/providers/cart_state.dart';
import 'package:worker/features/products/domain/entities/product.dart';
import 'package:worker/features/products/presentation/providers/products_provider.dart';
part 'cart_provider.g.dart';
@@ -46,8 +45,12 @@ class Cart extends _$Cart {
/// Initialize cart by loading from API
///
/// Call this from UI on mount to load cart items from backend.
/// Call this ONCE from HomePage on app startup.
/// Cart API returns product details, no need to fetch each product separately.
Future<void> initialize() async {
// Skip if already loaded
if (state.items.isNotEmpty) return;
final repository = await ref.read(cartRepositoryProvider.future);
// Set loading state
@@ -55,6 +58,7 @@ class Cart extends _$Cart {
try {
// Load cart items from API (with Hive fallback)
// Cart API returns: item_code, item_name, image, conversion_of_sm, quantity, amount
final cartItems = await repository.getCartItems();
// Get member tier from user profile
@@ -63,41 +67,47 @@ class Cart extends _$Cart {
const memberDiscountPercent = 15.0;
// Convert CartItem entities to CartItemData for UI
// Use product data from cart API directly - no need to fetch each product
final items = <CartItemData>[];
final selectedItems = <String, bool>{};
// Fetch product details for each cart item
final productsRepository = await ref.read(productsRepositoryProvider.future);
for (final cartItem in cartItems) {
try {
// Fetch full product entity from products repository
final product = await productsRepository.getProductById(cartItem.productId);
// Create minimal Product from cart item data (no need to fetch from API)
final now = DateTime.now();
final product = Product(
productId: cartItem.productId,
name: cartItem.itemName ?? cartItem.productId,
basePrice: cartItem.unitPrice,
images: cartItem.image != null ? [cartItem.image!] : [],
thumbnail: cartItem.image ?? '',
imageCaptions: const {},
specifications: const {},
conversionOfSm: cartItem.conversionOfSm,
erpnextItemCode: cartItem.productId,
isActive: true,
isFeatured: false,
createdAt: now,
updatedAt: now,
);
// Calculate conversion for this item
final converted = _calculateConversion(
cartItem.quantity,
product.conversionOfSm,
);
// Calculate conversion for this item
final converted = _calculateConversion(
cartItem.quantity,
product.conversionOfSm,
);
// Create CartItemData with full product info
items.add(
CartItemData(
product: product,
quantity: cartItem.quantity,
quantityConverted: converted.convertedQuantity,
boxes: converted.boxes,
),
);
// Create CartItemData with product info from cart API
items.add(
CartItemData(
product: product,
quantity: cartItem.quantity,
quantityConverted: converted.convertedQuantity,
boxes: converted.boxes,
),
);
// Initialize as not selected by default
selectedItems[product.productId] = false;
} catch (productError) {
// Skip this item if product can't be fetched
// In production, use a proper logging framework
// ignore: avoid_print
print('[CartProvider] Failed to load product ${cartItem.productId}: $productError');
}
// Initialize as not selected by default
selectedItems[product.productId] = false;
}
final newState = CartState(
@@ -150,6 +160,7 @@ class Cart extends _$Cart {
itemIds: [product.erpnextItemCode ?? product.productId],
quantities: [quantity],
prices: [product.basePrice],
conversionFactors: [product.conversionOfSm],
);
// Calculate conversion
@@ -332,6 +343,7 @@ class Cart extends _$Cart {
itemId: item.product.erpnextItemCode ?? productId,
quantity: quantity,
price: item.product.basePrice,
conversionFactor: item.product.conversionOfSm,
);
} catch (e) {
// Silent fail - keep local state, user can retry later
@@ -370,6 +382,7 @@ class Cart extends _$Cart {
itemId: item.product.erpnextItemCode ?? productId,
quantity: newQuantity,
price: item.product.basePrice,
conversionFactor: item.product.conversionOfSm,
);
// Update local state

View File

@@ -64,7 +64,7 @@ final class CartProvider extends $NotifierProvider<Cart, CartState> {
}
}
String _$cartHash() => r'706de28734e7059b2e9484f3b1d94226a0e90bb9';
String _$cartHash() => r'a12712db833127ef66b75f52cba8376409806afa';
/// Cart Notifier
///

View File

@@ -8,6 +8,7 @@ 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:worker/core/utils/extensions.dart';
import 'package:worker/core/widgets/loading_indicator.dart';
import 'package:worker/core/theme/typography.dart';
import 'package:worker/features/cart/presentation/providers/cart_provider.dart';
@@ -79,11 +80,6 @@ class _CartItemWidgetState extends ConsumerState<CartItemWidget> {
final isSelected =
cartState.selectedItems[widget.item.product.productId] ?? false;
final currencyFormatter = NumberFormat.currency(
locale: 'vi_VN',
symbol: 'đ',
decimalDigits: 0,
);
return Container(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
@@ -121,7 +117,11 @@ class _CartItemWidgetState extends ConsumerState<CartItemWidget> {
ClipRRect(
borderRadius: BorderRadius.circular(8),
child: CachedNetworkImage(
imageUrl: widget.item.product.thumbnail,
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,
@@ -168,7 +168,7 @@ class _CartItemWidgetState extends ConsumerState<CartItemWidget> {
// Price
Text(
'${currencyFormatter.format(widget.item.product.basePrice)}/m²',
'${widget.item.product.basePrice.toVNCurrency}/m²',
style: AppTypography.titleMedium.copyWith(
color: colorScheme.primary,
fontWeight: FontWeight.bold,