From 1fcef52d5ec52905fc6cd417b9f48aedb614114a Mon Sep 17 00:00:00 2001 From: Phuoc Nguyen Date: Thu, 20 Nov 2025 10:44:51 +0700 Subject: [PATCH] fix checkout/cart --- lib/core/router/app_router.dart | 8 +- lib/core/theme/app_theme.dart | 2 - .../cart/presentation/pages/cart_page.dart | 15 +- .../presentation/pages/checkout_page.dart | 238 +++++++++++++++--- .../widgets/checkout_submit_button.dart | 88 +++---- .../widgets/order_summary_section.dart | 18 +- .../widgets/price_negotiation_section.dart | 2 +- 7 files changed, 271 insertions(+), 100 deletions(-) diff --git a/lib/core/router/app_router.dart b/lib/core/router/app_router.dart index ffa3fa6..51dbc44 100644 --- a/lib/core/router/app_router.dart +++ b/lib/core/router/app_router.dart @@ -230,8 +230,12 @@ final routerProvider = Provider((ref) { GoRoute( path: RouteNames.checkout, name: RouteNames.checkout, - pageBuilder: (context, state) => - MaterialPage(key: state.pageKey, child: const CheckoutPage()), + pageBuilder: (context, state) => MaterialPage( + key: state.pageKey, + child: CheckoutPage( + checkoutData: state.extra as Map?, + ), + ), ), // Favorites Route diff --git a/lib/core/theme/app_theme.dart b/lib/core/theme/app_theme.dart index 1f6d85b..4c7c928 100644 --- a/lib/core/theme/app_theme.dart +++ b/lib/core/theme/app_theme.dart @@ -22,7 +22,6 @@ class AppTheme { tertiary: AppColors.accentCyan, error: AppColors.danger, surface: AppColors.white, - background: AppColors.grey50, ); return ThemeData( @@ -298,7 +297,6 @@ class AppTheme { tertiary: AppColors.primaryBlue, error: AppColors.danger, surface: const Color(0xFF1E1E1E), - background: const Color(0xFF121212), ); return ThemeData( diff --git a/lib/features/cart/presentation/pages/cart_page.dart b/lib/features/cart/presentation/pages/cart_page.dart index 09259ae..a4683a0 100644 --- a/lib/features/cart/presentation/pages/cart_page.dart +++ b/lib/features/cart/presentation/pages/cart_page.dart @@ -285,8 +285,19 @@ class _CartPageState extends ConsumerState { _isSyncing = false; }); - // Navigate to checkout - context.push(RouteNames.checkout); + // Get selected CartItemData objects + // Pass complete CartItemData to preserve conversion calculations + final selectedCartItems = cartState.items + .where((item) => + cartState.selectedItems[item.product.productId] == true) + .toList(); + + // Navigate to checkout with CartItemData + // Checkout page will fetch fresh pricing from API + context.push( + RouteNames.checkout, + extra: {'cartItems': selectedCartItems}, + ); } } : null, diff --git a/lib/features/cart/presentation/pages/checkout_page.dart b/lib/features/cart/presentation/pages/checkout_page.dart index 6206dee..54bffd8 100644 --- a/lib/features/cart/presentation/pages/checkout_page.dart +++ b/lib/features/cart/presentation/pages/checkout_page.dart @@ -17,7 +17,9 @@ import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:worker/core/constants/ui_constants.dart'; +import 'package:worker/core/theme/colors.dart'; import 'package:worker/features/account/domain/entities/address.dart'; +import 'package:worker/features/cart/presentation/providers/cart_state.dart'; import 'package:worker/features/cart/presentation/widgets/checkout_submit_button.dart'; import 'package:worker/features/cart/presentation/widgets/delivery_information_section.dart'; import 'package:worker/features/cart/presentation/widgets/invoice_section.dart'; @@ -29,7 +31,12 @@ import 'package:worker/features/cart/presentation/widgets/price_negotiation_sect /// /// Full checkout flow for placing orders. class CheckoutPage extends HookConsumerWidget { - const CheckoutPage({super.key}); + const CheckoutPage({ + super.key, + this.checkoutData, + }); + + final Map? checkoutData; @override Widget build(BuildContext context, WidgetRef ref) { @@ -50,36 +57,41 @@ class CheckoutPage extends HookConsumerWidget { // Price negotiation final needsNegotiation = useState(false); - // Mock cart items - final cartItems = useState>>([ - { - 'id': '1', - 'name': 'Gạch Granite 60x60 Marble White', - 'sku': 'GT-6060-MW', - 'quantity': 20, - 'price': 250000, - 'image': - 'https://images.unsplash.com/photo-1615971677499-5467cbab01c0?w=200', - }, - { - 'id': '2', - 'name': 'Gạch Ceramic 30x60 Wood Effect', - 'sku': 'CR-3060-WE', - 'quantity': 15, - 'price': 180000, - 'image': - 'https://images.unsplash.com/photo-1604709177225-055f99402ea3?w=200', - }, - ]); + // Get CartItemData from navigation + final cartItemsData = checkoutData?['cartItems'] as List? ?? []; - // Calculate totals - final subtotal = cartItems.value.fold( - 0, - (sum, item) => sum + (item['price'] as int) * (item['quantity'] as int), + // Convert CartItemData to Map format for OrderSummarySection + // Use all data directly from cart (no API calls needed) + final checkoutItems = cartItemsData.map((itemData) { + final cartItem = itemData as CartItemData; + return { + 'id': cartItem.product.productId, + 'name': cartItem.product.name, + 'sku': cartItem.product.erpnextItemCode ?? cartItem.product.productId, + 'quantity': cartItem.quantity, + 'quantityConverted': cartItem.quantityConverted, + 'boxes': cartItem.boxes, + 'price': cartItem.product.basePrice, + 'image': cartItem.product.images.isNotEmpty + ? cartItem.product.images.first + : null, + }; + }).toList(); + + // Calculate totals from cart data + final subtotal = checkoutItems.fold( + 0.0, + (sum, item) => sum + (item['price'] as double) * (item['quantityConverted'] as double), ); - final discount = subtotal * 0.05; // 5% discount - const shipping = 50000.0; - final total = subtotal - discount + shipping; + + // TODO: Fetch member discount from user profile API + const memberDiscountPercent = 15.0; // Diamond tier (temporary) + final memberDiscount = subtotal * (memberDiscountPercent / 100); + + // TODO: Fetch shipping fee from API based on address + const shipping = 0.0; // Free shipping (temporary) + + final total = subtotal - memberDiscount + shipping; return Scaffold( backgroundColor: const Color(0xFFF4F6F8), @@ -133,11 +145,16 @@ class CheckoutPage extends HookConsumerWidget { if (!needsNegotiation.value) const SizedBox(height: AppSpacing.md), + // Discount Code Section + _buildDiscountCodeSection(), + + const SizedBox(height: AppSpacing.md), + // Order Summary Section OrderSummarySection( - cartItems: cartItems.value, + cartItems: checkoutItems, subtotal: subtotal, - discount: discount, + discount: memberDiscount, shipping: shipping, total: total, ), @@ -149,6 +166,32 @@ class CheckoutPage extends HookConsumerWidget { const SizedBox(height: AppSpacing.md), + // Terms and Conditions + const Padding( + padding: EdgeInsets.symmetric(horizontal: AppSpacing.md), + child: Text.rich( + TextSpan( + text: 'Bằng cách đặt hàng, bạn đồng ý với ', + style: TextStyle( + fontSize: 14, + color: Color(0xFF6B7280), + ), + children: [ + TextSpan( + text: 'Điều khoản & Điều kiện', + style: TextStyle( + color: AppColors.primaryBlue, + decoration: TextDecoration.underline, + ), + ), + ], + ), + textAlign: TextAlign.center, + ), + ), + + const SizedBox(height: AppSpacing.md), + // Place Order Button CheckoutSubmitButton( formKey: formKey, @@ -166,4 +209,137 @@ class CheckoutPage extends HookConsumerWidget { ), ); } + + /// Build Discount Code Section (Card 4 from HTML) + Widget _buildDiscountCodeSection() { + return Container( + margin: const EdgeInsets.symmetric(horizontal: AppSpacing.md), + padding: const EdgeInsets.all(AppSpacing.md), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(AppRadius.card), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.05), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Section Title + Row( + children: [ + Icon( + FontAwesomeIcons.ticket, + color: AppColors.primaryBlue, + size: 20, + ), + const SizedBox(width: 8), + const Text( + 'Mã giảm giá', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Color(0xFF212121), + ), + ), + ], + ), + + const SizedBox(height: 12), + + // Input field with Apply button + Row( + children: [ + Expanded( + child: TextField( + decoration: InputDecoration( + hintText: 'Nhập mã giảm giá', + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: const BorderSide(color: Color(0xFFD1D5DB)), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: const BorderSide(color: Color(0xFFD1D5DB)), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + borderSide: const BorderSide( + color: AppColors.primaryBlue, + width: 2, + ), + ), + contentPadding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, + ), + ), + ), + ), + const SizedBox(width: 8), + ElevatedButton( + onPressed: () { + // TODO: Apply discount code + }, + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.primaryBlue, + padding: const EdgeInsets.symmetric( + horizontal: 24, + vertical: 12, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + elevation: 0, + ), + child: const Text( + 'Áp dụng', + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ), + + const SizedBox(height: 12), + + // Success banner (Diamond discount) + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: const Color(0xFFF0FDF4), + border: Border.all(color: const Color(0xFFBBF7D0)), + borderRadius: BorderRadius.circular(8), + ), + child: Row( + children: [ + Icon( + FontAwesomeIcons.circleCheck, + color: AppColors.success, + size: 18, + ), + const SizedBox(width: 8), + const Expanded( + child: Text( + 'Bạn được giảm 15% (hạng Diamond)', + style: TextStyle( + fontSize: 14, + color: Color(0xFF166534), + fontWeight: FontWeight.w500, + ), + ), + ), + ], + ), + ), + ], + ), + ); + } } diff --git a/lib/features/cart/presentation/widgets/checkout_submit_button.dart b/lib/features/cart/presentation/widgets/checkout_submit_button.dart index 68de7d9..2f1181c 100644 --- a/lib/features/cart/presentation/widgets/checkout_submit_button.dart +++ b/lib/features/cart/presentation/widgets/checkout_submit_button.dart @@ -34,61 +34,45 @@ class CheckoutSubmitButton extends StatelessWidget { @override Widget build(BuildContext context) { - return Padding( + return Container( + width: double.infinity, padding: const EdgeInsets.symmetric(horizontal: AppSpacing.md), - child: Column( - children: [ - // Terms Agreement Text - const Text( - 'Bằng việc đặt hàng, bạn đồng ý với các điều khoản và điều kiện của chúng tôi', - style: TextStyle(fontSize: 12, color: AppColors.grey500), - textAlign: TextAlign.center, - ), - - const SizedBox(height: AppSpacing.md), - - // Place Order / Send Negotiation Button - SizedBox( - width: double.infinity, - child: ElevatedButton( - onPressed: () { - // Validate address is selected - if (selectedAddress == null) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Vui lòng chọn địa chỉ giao hàng'), - backgroundColor: AppColors.danger, - duration: Duration(seconds: 2), - ), - ); - return; - } - - if (formKey.currentState?.validate() ?? false) { - _handlePlaceOrder(context); - } - }, - style: ElevatedButton.styleFrom( - backgroundColor: needsNegotiation - ? AppColors.warning - : AppColors.primaryBlue, - foregroundColor: Colors.white, - padding: const EdgeInsets.symmetric(vertical: 16), - elevation: 0, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(AppRadius.button), - ), + child: ElevatedButton( + onPressed: () { + // Validate address is selected + if (selectedAddress == null) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Vui lòng chọn địa chỉ giao hàng'), + backgroundColor: AppColors.danger, + duration: Duration(seconds: 2), ), - child: Text( - needsNegotiation ? 'Gửi yêu cầu đàm phán' : 'Đặt hàng', - style: const TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600, - ), - ), - ), + ); + return; + } + + if (formKey.currentState?.validate() ?? false) { + _handlePlaceOrder(context); + } + }, + style: ElevatedButton.styleFrom( + backgroundColor: needsNegotiation + ? AppColors.warning + : AppColors.primaryBlue, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 16), + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(AppRadius.button), ), - ], + ), + child: Text( + needsNegotiation ? 'Gửi yêu cầu đàm phán' : 'Đặt hàng', + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), ), ); } diff --git a/lib/features/cart/presentation/widgets/order_summary_section.dart b/lib/features/cart/presentation/widgets/order_summary_section.dart index d63699e..c9d8e32 100644 --- a/lib/features/cart/presentation/widgets/order_summary_section.dart +++ b/lib/features/cart/presentation/widgets/order_summary_section.dart @@ -105,11 +105,11 @@ class OrderSummarySection extends StatelessWidget { /// Build cart item with conversion details on two lines Widget _buildCartItemWithConversion(Map item) { - // Mock conversion data (in real app, this comes from CartItemData) - final quantity = item['quantity'] as int; - final quantityM2 = quantity.toDouble(); // User input - final quantityConverted = (quantityM2 * 1.008 * 100).ceil() / 100; // Rounded up - final boxes = (quantityM2 * 2.8).ceil(); // Tiles count + // Get real conversion data from CartItemData + final quantity = item['quantity'] as double; + final quantityConverted = item['quantityConverted'] as double; + final boxes = item['boxes'] as int; + final price = item['price'] as double; return Padding( padding: const EdgeInsets.only(bottom: 12), @@ -136,7 +136,7 @@ class OrderSummarySection extends StatelessWidget { const SizedBox(height: 4), // Line 2: Conversion details (muted text) Text( - '$quantityM2 m² ($boxes viên / ${quantityConverted.toStringAsFixed(2)} m²)', + '${quantity.toStringAsFixed(2)} m² ($boxes viên / ${quantityConverted.toStringAsFixed(2)} m²)', style: const TextStyle( fontSize: 13, color: AppColors.grey500, @@ -148,11 +148,9 @@ class OrderSummarySection extends StatelessWidget { const SizedBox(width: 12), - // Price (right side) + // Price (right side) - using converted quantity for accurate billing Text( - _formatCurrency( - ((item['price'] as int) * quantityConverted).toDouble(), - ), + _formatCurrency(price * quantityConverted), style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w600, diff --git a/lib/features/cart/presentation/widgets/price_negotiation_section.dart b/lib/features/cart/presentation/widgets/price_negotiation_section.dart index c097083..1f0a73d 100644 --- a/lib/features/cart/presentation/widgets/price_negotiation_section.dart +++ b/lib/features/cart/presentation/widgets/price_negotiation_section.dart @@ -12,9 +12,9 @@ import 'package:worker/core/theme/colors.dart'; /// /// Allows user to request price negotiation instead of direct order. class PriceNegotiationSection extends HookWidget { - final ValueNotifier needsNegotiation; const PriceNegotiationSection({super.key, required this.needsNegotiation}); + final ValueNotifier needsNegotiation; @override Widget build(BuildContext context) {