/// Cart Item Widget /// /// Displays a single item in the cart with checkbox, image, details, and quantity controls. library; 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: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'; /// Cart Item Widget /// /// Displays: /// - Checkbox for selection (left side, aligned to top) /// - Product image (100x100, rounded) /// - Product name and price /// - Quantity controls (-, text field for input, +, unit label) /// - Converted quantity display: "(Quy đổi: X.XX m² = Y viên)" class CartItemWidget extends ConsumerStatefulWidget { const CartItemWidget({super.key, required this.item}); final CartItemData item; @override ConsumerState createState() => _CartItemWidgetState(); } class _CartItemWidgetState extends ConsumerState { late TextEditingController _quantityController; late FocusNode _quantityFocusNode; @override void initState() { super.initState(); _quantityController = TextEditingController( text: widget.item.quantity.toStringAsFixed(0), ); _quantityFocusNode = FocusNode(); } @override void didUpdateWidget(CartItemWidget oldWidget) { super.didUpdateWidget(oldWidget); // Update text field when quantity changes from outside (increment/decrement buttons) if (widget.item.quantity != oldWidget.item.quantity) { _quantityController.text = widget.item.quantity.toStringAsFixed(0); } } @override void dispose() { _quantityController.dispose(); _quantityFocusNode.dispose(); super.dispose(); } void _handleQuantitySubmit(String value) { final newQuantity = double.tryParse(value); if (newQuantity != null && newQuantity >= 1) { ref .read(cartProvider.notifier) .updateQuantity(widget.item.product.productId, newQuantity); } else { // Invalid input, reset to current quantity _quantityController.text = widget.item.quantity.toStringAsFixed(0); } _quantityFocusNode.unfocus(); } @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; final cartState = ref.watch(cartProvider); 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), padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: colorScheme.surface, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.05), blurRadius: 4, offset: const Offset(0, 2), ), ], ), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Checkbox (aligned to top, ~50px from top to match HTML) Padding( padding: const EdgeInsets.only(top: 34), child: _CustomCheckbox( value: isSelected, onChanged: (value) { ref .read(cartProvider.notifier) .toggleSelection(widget.item.product.productId); }, ), ), const SizedBox(width: 12), // Product Image (bigger: 100x100) ClipRRect( borderRadius: BorderRadius.circular(8), child: CachedNetworkImage( imageUrl: widget.item.product.thumbnail, width: 100, height: 100, fit: BoxFit.cover, placeholder: (context, url) => Container( width: 100, height: 100, color: colorScheme.surfaceContainerHighest, child: const Center( child: CircularProgressIndicator(strokeWidth: 2), ), ), errorWidget: (context, url, error) => Container( width: 100, height: 100, color: colorScheme.surfaceContainerHighest, child: FaIcon( FontAwesomeIcons.image, color: colorScheme.onSurfaceVariant, size: 32, ), ), ), ), const SizedBox(width: 12), // Product Info Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Product Name Text( widget.item.product.name, style: AppTypography.titleMedium.copyWith( fontWeight: FontWeight.w600, fontSize: 15, ), maxLines: 2, overflow: TextOverflow.ellipsis, ), const SizedBox(height: 4), // Price Text( '${currencyFormatter.format(widget.item.product.basePrice)}/m²', style: AppTypography.titleMedium.copyWith( color: colorScheme.primary, fontWeight: FontWeight.bold, fontSize: 16, ), ), const SizedBox(height: 8), // Quantity Controls Row( children: [ // Decrease button _QuantityButton( icon: FontAwesomeIcons.minus, onPressed: () { ref .read(cartProvider.notifier) .decrementQuantity(widget.item.product.productId); }, ), const SizedBox(width: 8), // Quantity TextField SizedBox( width: 50, height: 32, child: TextField( controller: _quantityController, focusNode: _quantityFocusNode, keyboardType: TextInputType.number, textAlign: TextAlign.center, style: AppTypography.titleMedium.copyWith( fontWeight: FontWeight.w600, fontSize: 16, ), decoration: InputDecoration( contentPadding: EdgeInsets.zero, border: OutlineInputBorder( borderRadius: BorderRadius.circular(6), borderSide: BorderSide( color: colorScheme.outlineVariant, width: 1, ), ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(6), borderSide: BorderSide( color: colorScheme.outlineVariant, width: 1, ), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(6), borderSide: BorderSide( color: colorScheme.primary, width: 2, ), ), ), onSubmitted: _handleQuantitySubmit, onEditingComplete: () { _handleQuantitySubmit(_quantityController.text); }, ), ), const SizedBox(width: 8), // Increase button _QuantityButton( icon: FontAwesomeIcons.plus, onPressed: () { ref .read(cartProvider.notifier) .incrementQuantity(widget.item.product.productId); }, ), const SizedBox(width: 8), // Unit label Text( 'm²', style: AppTypography.bodySmall.copyWith( color: colorScheme.onSurfaceVariant, ), ), ], ), const SizedBox(height: 4), // Converted Quantity Display RichText( text: TextSpan( style: AppTypography.bodySmall.copyWith( color: colorScheme.onSurfaceVariant, fontSize: 13, ), children: [ const TextSpan(text: '(Quy đổi: '), TextSpan( text: '${widget.item.quantityConverted.toStringAsFixed(2)} m²', style: const TextStyle(fontWeight: FontWeight.bold), ), const TextSpan(text: ' = '), TextSpan( text: '${widget.item.boxes} viên', style: const TextStyle(fontWeight: FontWeight.bold), ), const TextSpan(text: ')'), ], ), ), ], ), ), ], ), ); } } /// Custom Checkbox Widget /// /// Matches HTML design with 20px 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) { final colorScheme = Theme.of(context).colorScheme; return GestureDetector( onTap: () => onChanged?.call(!value), child: Container( width: 20, height: 20, decoration: BoxDecoration( color: value ? colorScheme.primary : colorScheme.surface, border: Border.all( color: value ? colorScheme.primary : colorScheme.outlineVariant, width: 2, ), borderRadius: BorderRadius.circular(6), ), child: value ? Icon( FontAwesomeIcons.check, size: 14, color: colorScheme.surface, ) : null, ), ); } } /// Quantity Button /// /// Small button for incrementing/decrementing quantity. class _QuantityButton extends StatelessWidget { const _QuantityButton({required this.icon, required this.onPressed}); final IconData icon; final VoidCallback onPressed; @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; return InkWell( onTap: onPressed, borderRadius: BorderRadius.circular(6), child: Container( width: 32, height: 32, decoration: BoxDecoration( border: Border.all(color: colorScheme.outlineVariant, width: 2), borderRadius: BorderRadius.circular(6), color: colorScheme.surface, ), child: Icon(icon, size: 16, color: colorScheme.onSurface), ), ); } }