/// Cart Page /// /// Shopping cart screen with selection and checkout. /// Features expanded item list with total price at bottom. library; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:go_router/go_router.dart'; import 'package:intl/intl.dart'; import 'package:worker/core/constants/ui_constants.dart'; import 'package:worker/core/router/app_router.dart'; import 'package:worker/core/theme/colors.dart'; import 'package:worker/core/theme/typography.dart'; import 'package:worker/features/cart/presentation/providers/cart_provider.dart'; import 'package:worker/features/cart/presentation/providers/cart_state.dart'; import 'package:worker/features/cart/presentation/widgets/cart_item_widget.dart'; /// Cart Page /// /// Features: /// - AppBar with back, title (with count), and delete button /// - Select all section with count display /// - Expanded cart items list with checkboxes /// - Total price and checkout button at bottom class CartPage extends ConsumerStatefulWidget { const CartPage({super.key}); @override ConsumerState createState() => _CartPageState(); } class _CartPageState extends ConsumerState { bool _isSyncing = false; @override void initState() { super.initState(); // Initialize cart from API on mount WidgetsBinding.instance.addPostFrameCallback((_) { ref.read(cartProvider.notifier).initialize(); }); } // Note: Sync is handled in PopScope.onPopInvokedWithResult for back navigation // and in checkout button handler for checkout flow. // No dispose() method needed - using ref.read() in dispose() is unsafe. @override Widget build(BuildContext context) { final cartState = ref.watch(cartProvider); final currencyFormatter = NumberFormat.currency( locale: 'vi_VN', symbol: 'đ', decimalDigits: 0, ); final itemCount = cartState.itemCount; final hasSelection = cartState.selectedCount > 0; return PopScope( // Intercept back navigation to sync pending updates onPopInvokedWithResult: (didPop, result) async { if (didPop) { // Sync any pending quantity updates before leaving the page await ref.read(cartProvider.notifier).forceSyncPendingUpdates(); } }, child: Scaffold( backgroundColor: const Color(0xFFF4F6F8), appBar: AppBar( leading: IconButton( icon: const FaIcon(FontAwesomeIcons.arrowLeft, color: Colors.black, size: 20), onPressed: () => context.pop(), ), title: Text( 'Giỏ hàng ($itemCount)', style: const TextStyle(color: Colors.black), ), elevation: AppBarSpecs.elevation, backgroundColor: AppColors.white, foregroundColor: AppColors.grey900, centerTitle: false, actions: [ if (cartState.isNotEmpty) IconButton( icon: Icon( FontAwesomeIcons.trashCan, color: hasSelection ? AppColors.danger : AppColors.grey500, ), onPressed: hasSelection ? () { _showDeleteConfirmation(context, ref, cartState); } : null, tooltip: 'Xóa sản phẩm đã chọn', ), const SizedBox(width: AppSpacing.sm), ], ), body: cartState.isLoading && cartState.isEmpty ? const Center(child: CircularProgressIndicator()) : cartState.errorMessage != null && cartState.isEmpty ? _buildErrorState(context, cartState.errorMessage!) : cartState.isEmpty ? _buildEmptyCart(context) : Column( children: [ // Error banner if there's an error if (cartState.errorMessage != null) _buildErrorBanner(cartState.errorMessage!), // Select All Section const SizedBox(height: 8), _buildSelectAllSection(cartState, ref), const SizedBox(height: 8), // Expanded Cart Items List Expanded( child: Stack( children: [ ListView.builder( itemCount: cartState.items.length, itemBuilder: (context, index) { return CartItemWidget(item: cartState.items[index]); }, ), // Loading overlay if (cartState.isLoading) Container( color: Colors.black.withValues(alpha: 0.1), child: const Center( child: CircularProgressIndicator(), ), ), ], ), ), // Total and Checkout at Bottom _buildBottomSection( context, cartState, ref, currencyFormatter, hasSelection, ), ], ), ), ); } /// Build select all section Widget _buildSelectAllSection(CartState cartState, WidgetRef ref) { return Container( margin: const EdgeInsets.symmetric(horizontal: 16), padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16), decoration: BoxDecoration( color: AppColors.white, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.05), blurRadius: 4, offset: const Offset(0, 2), ), ], ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ // Checkbox with label GestureDetector( onTap: () { ref.read(cartProvider.notifier).toggleSelectAll(); }, child: Row( children: [ _CustomCheckbox( value: cartState.isAllSelected, onChanged: (value) { ref.read(cartProvider.notifier).toggleSelectAll(); }, ), const SizedBox(width: 12), Text( 'Chọn tất cả', style: AppTypography.titleMedium.copyWith( fontWeight: FontWeight.w600, ), ), ], ), ), // Selected count Text( 'Đã chọn: ${cartState.selectedCount}/${cartState.itemCount}', style: AppTypography.bodyMedium.copyWith( color: AppColors.primaryBlue, fontWeight: FontWeight.w600, fontSize: 14, ), ), ], ), ); } /// Build bottom section with total price and checkout button Widget _buildBottomSection( BuildContext context, CartState cartState, WidgetRef ref, NumberFormat currencyFormatter, bool hasSelection, ) { return Container( decoration: BoxDecoration( color: AppColors.white, border: const Border( top: BorderSide(color: Color(0xFFF0F0F0), width: 2), ), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.08), blurRadius: 10, offset: const Offset(0, -2), ), ], ), child: SafeArea( child: Padding( padding: const EdgeInsets.all(16), child: Column( mainAxisSize: MainAxisSize.min, children: [ // Total Price Row Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( 'Tổng tạm tính (${cartState.selectedCount} sản phẩm)', style: AppTypography.bodyMedium.copyWith( color: AppColors.grey500, fontSize: 14, ), ), Text( currencyFormatter.format(cartState.selectedTotal), style: AppTypography.headlineSmall.copyWith( color: AppColors.primaryBlue, fontWeight: FontWeight.bold, fontSize: 20, ), ), ], ), const SizedBox(height: 16), // Checkout Button SizedBox( width: double.infinity, height: 48, child: ElevatedButton( onPressed: hasSelection && !_isSyncing ? () async { // Set syncing state setState(() { _isSyncing = true; }); // Force sync any pending quantity updates before checkout await ref .read(cartProvider.notifier) .forceSyncPendingUpdates(); // Reset syncing state if (mounted) { setState(() { _isSyncing = false; }); // Navigate to checkout context.push(RouteNames.checkout); } } : null, style: ElevatedButton.styleFrom( backgroundColor: AppColors.primaryBlue, disabledBackgroundColor: AppColors.grey100, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10), ), elevation: 0, ), child: _isSyncing ? const SizedBox( width: 20, height: 20, child: CircularProgressIndicator( strokeWidth: 2, valueColor: AlwaysStoppedAnimation(AppColors.white), ), ) : Text( 'Tiến hành đặt hàng', style: AppTypography.labelLarge.copyWith( color: AppColors.white, fontWeight: FontWeight.w600, fontSize: 16, ), ), ), ), ], ), ), ), ); } /// Build error banner (shown at top when there's an error but cart has items) Widget _buildErrorBanner(String errorMessage) { return Container( width: double.infinity, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), color: AppColors.danger.withValues(alpha: 0.1), child: Row( children: [ const FaIcon(FontAwesomeIcons.circleExclamation, color: AppColors.danger, size: 18), const SizedBox(width: 8), Expanded( child: Text( errorMessage, style: AppTypography.bodySmall.copyWith(color: AppColors.danger), ), ), ], ), ); } /// Build error state (shown when cart fails to load and is empty) Widget _buildErrorState(BuildContext context, String errorMessage) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const FaIcon(FontAwesomeIcons.circleExclamation, size: 56, color: AppColors.danger), const SizedBox(height: 16), const Text( 'Không thể tải giỏ hàng', style: AppTypography.headlineMedium, ), const SizedBox(height: 8), Padding( padding: const EdgeInsets.symmetric(horizontal: 32), child: Text( errorMessage, style: AppTypography.bodyMedium.copyWith(color: AppColors.grey500), textAlign: TextAlign.center, ), ), const SizedBox(height: 24), ElevatedButton.icon( onPressed: () { ref.read(cartProvider.notifier).initialize(); }, icon: const FaIcon(FontAwesomeIcons.arrowsRotate, size: 20), label: const Text('Thử lại'), ), ], ), ); } /// Build empty cart state Widget _buildEmptyCart(BuildContext context) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( FontAwesomeIcons.cartShopping, size: 80, color: AppColors.grey500.withValues(alpha: 0.5), ), const SizedBox(height: 16), Text( 'Giỏ hàng trống', style: AppTypography.headlineMedium.copyWith( color: AppColors.grey500, ), ), const SizedBox(height: 8), Text( 'Hãy thêm sản phẩm vào giỏ hàng', style: AppTypography.bodyMedium.copyWith(color: AppColors.grey500), ), const SizedBox(height: 24), ElevatedButton.icon( onPressed: () => context.go(RouteNames.products), icon: const FaIcon(FontAwesomeIcons.bagShopping, size: 20), label: const Text('Xem sản phẩm'), ), ], ), ); } /// Show delete confirmation dialog void _showDeleteConfirmation( BuildContext context, WidgetRef ref, CartState cartState, ) { showDialog( context: context, builder: (context) => AlertDialog( title: const Text('Xóa sản phẩm'), content: Text( 'Bạn có chắc muốn xóa ${cartState.selectedCount} sản phẩm đã chọn?', ), actions: [ TextButton( onPressed: () => context.pop(), child: const Text('Hủy'), ), ElevatedButton( onPressed: () { ref.read(cartProvider.notifier).deleteSelected(); context.pop(); ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('Đã xóa sản phẩm khỏi giỏ hàng'), backgroundColor: AppColors.success, duration: Duration(seconds: 2), ), ); }, style: ElevatedButton.styleFrom(backgroundColor: AppColors.danger), child: const Text('Xóa'), ), ], ), ); } } /// Custom Checkbox Widget /// /// Matches HTML design with 22px size, 6px radius, blue when checked. class _CustomCheckbox extends StatelessWidget { const _CustomCheckbox({required this.value, this.onChanged}); final bool value; final ValueChanged? onChanged; @override Widget build(BuildContext context) { return GestureDetector( onTap: () => onChanged?.call(!value), child: Container( width: 22, height: 22, decoration: BoxDecoration( color: value ? AppColors.primaryBlue : AppColors.white, border: Border.all( color: value ? AppColors.primaryBlue : const Color(0xFFCBD5E1), width: 2, ), borderRadius: BorderRadius.circular(6), ), child: value ? const Icon( FontAwesomeIcons.check, size: 16, color: AppColors.white, ) : null, ), ); } }