Files
worker/CART_CODE_REFERENCE.md
Phuoc Nguyen aae3c9d080 update cart
2025-11-14 16:19:25 +07:00

11 KiB

Cart Feature - Key Code Reference

1. Adding Item to Cart with Conversion

// In cart_provider.dart
void addToCart(Product product, {double quantity = 1.0}) {
  // Calculate conversion
  final converted = _calculateConversion(quantity);
  
  // Create cart item with conversion data
  final newItem = CartItemData(
    product: product,
    quantity: quantity,                    // User input: 10
    quantityConverted: converted.convertedQuantity,  // Billing: 10.08
    boxes: converted.boxes,                // Tiles: 28
  );
  
  // Add to cart and auto-select
  final updatedSelection = Map<String, bool>.from(state.selectedItems);
  updatedSelection[product.productId] = true;
  
  state = state.copyWith(
    items: [...state.items, newItem],
    selectedItems: updatedSelection,
  );
}

// Conversion calculation (mock - replace with backend)
({double convertedQuantity, int boxes}) _calculateConversion(double quantity) {
  final converted = (quantity * 1.008 * 100).ceilToDouble() / 100;
  final boxes = (quantity * 2.8).ceil();
  return (convertedQuantity: converted, boxes: boxes);
}

2. Cart Item Widget with Checkbox

// In cart_item_widget.dart
Row(
  crossAxisAlignment: CrossAxisAlignment.start,
  children: [
    // Checkbox (aligned to top)
    Padding(
      padding: const EdgeInsets.only(top: 34),
      child: _CustomCheckbox(
        value: isSelected,
        onChanged: (value) {
          ref.read(cartProvider.notifier).toggleSelection(item.product.productId);
        },
      ),
    ),
    
    const SizedBox(width: 12),
    
    // Product Image
    ClipRRect(...),
    
    const SizedBox(width: 12),
    
    // Product Info with Conversion
    Expanded(
      child: Column(
        children: [
          Text(item.product.name),
          Text('${price}/${unit}'),
          
          // Quantity Controls
          Row([
            _QuantityButton(icon: Icons.remove, onPressed: decrement),
            Text(quantity),
            _QuantityButton(icon: Icons.add, onPressed: increment),
            Text(unit),
          ]),
          
          // Conversion Display
          RichText(
            text: TextSpan(
              children: [
                TextSpan(text: '(Quy đổi: '),
                TextSpan(
                  text: '${item.quantityConverted.toStringAsFixed(2)} m²',
                  style: TextStyle(fontWeight: FontWeight.bold),
                ),
                TextSpan(text: ' = '),
                TextSpan(
                  text: '${item.boxes} viên',
                  style: TextStyle(fontWeight: FontWeight.bold),
                ),
                TextSpan(text: ')'),
              ],
            ),
          ),
        ],
      ),
    ),
  ],
)

3. Select All Section

// In cart_page.dart
Container(
  child: Row(
    mainAxisAlignment: MainAxisAlignment.spaceBetween,
    children: [
      // Left: Checkbox + 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ả'),
          ],
        ),
      ),
      
      // Right: Selected Count
      Text('Đã chọn: ${cartState.selectedCount}/${cartState.itemCount}'),
    ],
  ),
)
// In cart_page.dart
Positioned(
  bottom: 0,
  left: 0,
  right: 0,
  child: Container(
    decoration: BoxDecoration(
      color: AppColors.white,
      border: Border(top: BorderSide(...)),
      boxShadow: [...],
    ),
    child: SafeArea(
      child: Row(
        children: [
          // Delete Button (48x48)
          InkWell(
            onTap: hasSelection ? deleteSelected : null,
            child: Container(
              width: 48,
              height: 48,
              decoration: BoxDecoration(
                border: Border.all(color: AppColors.danger, width: 2),
                borderRadius: BorderRadius.circular(10),
              ),
              child: Icon(Icons.delete_outline),
            ),
          ),
          
          const SizedBox(width: 16),
          
          // Total Info
          Expanded(
            child: Column(
              children: [
                Text('Tổng tạm tính (${selectedCount} sản phẩm)'),
                Text(currencyFormatter.format(selectedTotal)),
              ],
            ),
          ),
          
          const SizedBox(width: 16),
          
          // Checkout Button
          ElevatedButton(
            onPressed: hasSelection ? checkout : null,
            child: Text('Tiến hành đặt hàng'),
          ),
        ],
      ),
    ),
  ),
)

5. Selection Logic in Provider

// Toggle single item
void toggleSelection(String productId) {
  final updatedSelection = Map<String, bool>.from(state.selectedItems);
  updatedSelection[productId] = !(updatedSelection[productId] ?? false);
  state = state.copyWith(selectedItems: updatedSelection);
  _recalculateTotal();
}

// Toggle all items
void toggleSelectAll() {
  final allSelected = state.isAllSelected;
  final updatedSelection = <String, bool>{};
  for (final item in state.items) {
    updatedSelection[item.product.productId] = !allSelected;
  }
  state = state.copyWith(selectedItems: updatedSelection);
  _recalculateTotal();
}

// Delete selected
void deleteSelected() {
  final selectedIds = state.selectedItems.entries
      .where((entry) => entry.value)
      .map((entry) => entry.key)
      .toSet();

  final remainingItems = state.items
      .where((item) => !selectedIds.contains(item.product.productId))
      .toList();

  final updatedSelection = Map<String, bool>.from(state.selectedItems);
  for (final id in selectedIds) {
    updatedSelection.remove(id);
  }

  state = state.copyWith(
    items: remainingItems,
    selectedItems: updatedSelection,
  );
  _recalculateTotal();
}

6. Recalculate Total (Selected Items Only)

void _recalculateTotal() {
  // Only include selected items
  double subtotal = 0.0;
  for (final item in state.items) {
    if (state.selectedItems[item.product.productId] == true) {
      subtotal += item.lineTotal; // Uses quantityConverted
    }
  }

  final memberDiscount = subtotal * (state.memberDiscountPercent / 100);
  const shippingFee = 0.0;
  final total = subtotal - memberDiscount + shippingFee;

  state = state.copyWith(
    subtotal: subtotal,
    memberDiscount: memberDiscount,
    shippingFee: shippingFee,
    total: total,
  );
}

7. Payment Method Options

// Full Payment
Radio<String>(
  value: 'full_payment',
  groupValue: paymentMethod.value,
  onChanged: (value) => paymentMethod.value = value!,
),
const Column(
  children: [
    Text('Thanh toán hoàn toàn'),
    Text('Thanh toán qua tài khoản ngân hàng'),
  ],
),

// Partial Payment
Radio<String>(
  value: 'partial_payment',
  groupValue: paymentMethod.value,
  onChanged: (value) => paymentMethod.value = value!,
),
const Column(
  children: [
    Text('Thanh toán một phần'),
    Text('Trả trước(≥20%), còn lại thanh toán trong vòng 30 ngày'),
  ],
),

8. Order Summary with Conversion

// Item display in checkout
Row(
  children: [
    Expanded(
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          // Line 1: Product name
          Text(item['name']),
          
          // Line 2: Conversion (muted)
          Text(
            '$quantityM2 m² ($boxes viên / ${quantityConverted.toStringAsFixed(2)} m²)',
            style: TextStyle(color: AppColors.grey500),
          ),
        ],
      ),
    ),
    
    // Price (using converted quantity)
    Text(_formatCurrency(price * quantityConverted)),
  ],
)

9. Custom Checkbox Widget

class _CustomCheckbox extends StatelessWidget {
  final bool value;
  final ValueChanged<bool?>? 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 : Color(0xFFCBD5E1),
            width: 2,
          ),
          borderRadius: BorderRadius.circular(6),
        ),
        child: value
            ? Icon(Icons.check, size: 16, color: AppColors.white)
            : null,
      ),
    );
  }
}

10. Delete Confirmation Dialog

void _showDeleteConfirmation(BuildContext context, WidgetRef ref, CartState cartState) {
  showDialog<void>(
    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,
              ),
            );
          },
          style: ElevatedButton.styleFrom(backgroundColor: AppColors.danger),
          child: const Text('Xóa'),
        ),
      ],
    ),
  );
}

CSS/Flutter Equivalents

HTML Checkbox Styles → Flutter

/* HTML */
.checkmark {
  height: 22px;
  width: 22px;
  border: 2px solid #cbd5e1;
  border-radius: 6px;
}

.checkbox-container input:checked ~ .checkmark {
  background-color: #005B9A;
  border-color: #005B9A;
}
// Flutter
Container(
  width: 22,
  height: 22,
  decoration: BoxDecoration(
    color: value ? AppColors.primaryBlue : AppColors.white,
    border: Border.all(
      color: value ? AppColors.primaryBlue : Color(0xFFCBD5E1),
      width: 2,
    ),
    borderRadius: BorderRadius.circular(6),
  ),
  child: value ? Icon(Icons.check, size: 16, color: AppColors.white) : null,
)
/* HTML */
.cart-footer {
  position: fixed;
  bottom: 0;
  background: white;
  border-top: 2px solid #f0f0f0;
  box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.08);
  z-index: 100;
}
// Flutter
Positioned(
  bottom: 0,
  left: 0,
  right: 0,
  child: Container(
    decoration: BoxDecoration(
      color: AppColors.white,
      border: Border(top: BorderSide(color: Color(0xFFF0F0F0), width: 2)),
      boxShadow: [
        BoxShadow(
          color: Colors.black.withValues(alpha: 0.08),
          blurRadius: 10,
          offset: Offset(0, -2),
        ),
      ],
    ),
    child: SafeArea(child: /* footer content */),
  ),
)