453 lines
11 KiB
Markdown
453 lines
11 KiB
Markdown
# 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 */),
|
|
),
|
|
)
|
|
```
|