update cart
This commit is contained in:
452
CART_CODE_REFERENCE.md
Normal file
452
CART_CODE_REFERENCE.md
Normal file
@@ -0,0 +1,452 @@
|
||||
# Cart Feature - Key Code Reference
|
||||
|
||||
## 1. Adding Item to Cart with Conversion
|
||||
|
||||
```dart
|
||||
// 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
|
||||
|
||||
```dart
|
||||
// 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
|
||||
|
||||
```dart
|
||||
// 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}'),
|
||||
],
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
## 4. Sticky Footer
|
||||
|
||||
```dart
|
||||
// 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
|
||||
|
||||
```dart
|
||||
// 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)
|
||||
|
||||
```dart
|
||||
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
|
||||
|
||||
```dart
|
||||
// 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
|
||||
|
||||
```dart
|
||||
// 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
|
||||
|
||||
```dart
|
||||
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
|
||||
|
||||
```dart
|
||||
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
|
||||
```css
|
||||
/* HTML */
|
||||
.checkmark {
|
||||
height: 22px;
|
||||
width: 22px;
|
||||
border: 2px solid #cbd5e1;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.checkbox-container input:checked ~ .checkmark {
|
||||
background-color: #005B9A;
|
||||
border-color: #005B9A;
|
||||
}
|
||||
```
|
||||
|
||||
```dart
|
||||
// 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 Sticky Footer → Flutter
|
||||
```css
|
||||
/* 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;
|
||||
}
|
||||
```
|
||||
|
||||
```dart
|
||||
// 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 */),
|
||||
),
|
||||
)
|
||||
```
|
||||
Reference in New Issue
Block a user