update cart

This commit is contained in:
Phuoc Nguyen
2025-11-14 16:19:25 +07:00
parent 4738553d2e
commit aae3c9d080
30 changed files with 5954 additions and 758 deletions

452
CART_CODE_REFERENCE.md Normal file
View 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 */),
),
)
```