add auth, format
This commit is contained in:
@@ -76,12 +76,7 @@ class Cart {
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return Object.hash(
|
||||
cartId,
|
||||
userId,
|
||||
totalAmount,
|
||||
isSynced,
|
||||
);
|
||||
return Object.hash(cartId, userId, totalAmount, isSynced);
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -48,19 +48,14 @@ class _CartPageState extends ConsumerState<CartPage> {
|
||||
title: const Text('Xóa giỏ hàng'),
|
||||
content: const Text('Bạn có chắc chắn muốn xóa toàn bộ giỏ hàng?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => context.pop(),
|
||||
child: const Text('Hủy'),
|
||||
),
|
||||
TextButton(onPressed: () => context.pop(), child: const Text('Hủy')),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
ref.read(cartProvider.notifier).clearCart();
|
||||
context.pop();
|
||||
context.pop(); // Also go back from cart page
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.danger,
|
||||
),
|
||||
style: ElevatedButton.styleFrom(backgroundColor: AppColors.danger),
|
||||
child: const Text('Xóa'),
|
||||
),
|
||||
],
|
||||
@@ -86,7 +81,10 @@ class _CartPageState extends ConsumerState<CartPage> {
|
||||
icon: const Icon(Icons.arrow_back, color: Colors.black),
|
||||
onPressed: () => context.pop(),
|
||||
),
|
||||
title: Text('Giỏ hàng ($itemCount)', style: const TextStyle(color: Colors.black)),
|
||||
title: Text(
|
||||
'Giỏ hàng ($itemCount)',
|
||||
style: const TextStyle(color: Colors.black),
|
||||
),
|
||||
elevation: AppBarSpecs.elevation,
|
||||
backgroundColor: AppColors.white,
|
||||
foregroundColor: AppColors.grey900,
|
||||
@@ -155,9 +153,7 @@ class _CartPageState extends ConsumerState<CartPage> {
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Hãy thêm sản phẩm vào giỏ hàng',
|
||||
style: AppTypography.bodyMedium.copyWith(
|
||||
color: AppColors.grey500,
|
||||
),
|
||||
style: AppTypography.bodyMedium.copyWith(color: AppColors.grey500),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
ElevatedButton.icon(
|
||||
@@ -283,9 +279,9 @@ class _CartPageState extends ConsumerState<CartPage> {
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
if (_discountController.text.isNotEmpty) {
|
||||
ref.read(cartProvider.notifier).applyDiscountCode(
|
||||
_discountController.text,
|
||||
);
|
||||
ref
|
||||
.read(cartProvider.notifier)
|
||||
.applyDiscountCode(_discountController.text);
|
||||
}
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
@@ -326,7 +322,10 @@ class _CartPageState extends ConsumerState<CartPage> {
|
||||
}
|
||||
|
||||
/// Build order summary section
|
||||
Widget _buildOrderSummary(CartState cartState, NumberFormat currencyFormatter) {
|
||||
Widget _buildOrderSummary(
|
||||
CartState cartState,
|
||||
NumberFormat currencyFormatter,
|
||||
) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
padding: const EdgeInsets.all(16),
|
||||
@@ -394,10 +393,7 @@ class _CartPageState extends ConsumerState<CartPage> {
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'Phí vận chuyển',
|
||||
style: AppTypography.bodyMedium,
|
||||
),
|
||||
Text('Phí vận chuyển', style: AppTypography.bodyMedium),
|
||||
Text(
|
||||
cartState.shippingFee > 0
|
||||
? currencyFormatter.format(cartState.shippingFee)
|
||||
@@ -448,10 +444,7 @@ class _CartPageState extends ConsumerState<CartPage> {
|
||||
: null,
|
||||
child: const Text(
|
||||
'Tiến hành đặt hàng',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -142,11 +142,10 @@ class CheckoutPage extends HookConsumerWidget {
|
||||
|
||||
// Payment Method Section (hidden if negotiation is checked)
|
||||
if (!needsNegotiation.value)
|
||||
PaymentMethodSection(
|
||||
paymentMethod: paymentMethod,
|
||||
),
|
||||
PaymentMethodSection(paymentMethod: paymentMethod),
|
||||
|
||||
if (!needsNegotiation.value) const SizedBox(height: AppSpacing.md),
|
||||
if (!needsNegotiation.value)
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
|
||||
// Order Summary Section
|
||||
OrderSummarySection(
|
||||
@@ -160,9 +159,7 @@ class CheckoutPage extends HookConsumerWidget {
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
|
||||
// Price Negotiation Section
|
||||
PriceNegotiationSection(
|
||||
needsNegotiation: needsNegotiation,
|
||||
),
|
||||
PriceNegotiationSection(needsNegotiation: needsNegotiation),
|
||||
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
|
||||
|
||||
@@ -44,14 +44,9 @@ class Cart extends _$Cart {
|
||||
);
|
||||
} else {
|
||||
// Add new item
|
||||
final newItem = CartItemData(
|
||||
product: product,
|
||||
quantity: quantity,
|
||||
);
|
||||
final newItem = CartItemData(product: product, quantity: quantity);
|
||||
|
||||
state = state.copyWith(
|
||||
items: [...state.items, newItem],
|
||||
);
|
||||
state = state.copyWith(items: [...state.items, newItem]);
|
||||
_recalculateTotal();
|
||||
}
|
||||
}
|
||||
@@ -59,7 +54,9 @@ class Cart extends _$Cart {
|
||||
/// Remove product from cart
|
||||
void removeFromCart(String productId) {
|
||||
state = state.copyWith(
|
||||
items: state.items.where((item) => item.product.productId != productId).toList(),
|
||||
items: state.items
|
||||
.where((item) => item.product.productId != productId)
|
||||
.toList(),
|
||||
);
|
||||
_recalculateTotal();
|
||||
}
|
||||
@@ -113,20 +110,14 @@ class Cart extends _$Cart {
|
||||
// TODO: Validate with backend
|
||||
// For now, simulate discount application
|
||||
if (code.isNotEmpty) {
|
||||
state = state.copyWith(
|
||||
discountCode: code,
|
||||
discountCodeApplied: true,
|
||||
);
|
||||
state = state.copyWith(discountCode: code, discountCodeApplied: true);
|
||||
_recalculateTotal();
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove discount code
|
||||
void removeDiscountCode() {
|
||||
state = state.copyWith(
|
||||
discountCode: null,
|
||||
discountCodeApplied: false,
|
||||
);
|
||||
state = state.copyWith(discountCode: null, discountCodeApplied: false);
|
||||
_recalculateTotal();
|
||||
}
|
||||
|
||||
@@ -157,10 +148,7 @@ class Cart extends _$Cart {
|
||||
|
||||
/// Get total quantity of all items
|
||||
double get totalQuantity {
|
||||
return state.items.fold<double>(
|
||||
0.0,
|
||||
(sum, item) => sum + item.quantity,
|
||||
);
|
||||
return state.items.fold<double>(0.0, (sum, item) => sum + item.quantity);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,18 +12,12 @@ class CartItemData {
|
||||
final Product product;
|
||||
final double quantity;
|
||||
|
||||
const CartItemData({
|
||||
required this.product,
|
||||
required this.quantity,
|
||||
});
|
||||
const CartItemData({required this.product, required this.quantity});
|
||||
|
||||
/// Calculate line total
|
||||
double get lineTotal => product.basePrice * quantity;
|
||||
|
||||
CartItemData copyWith({
|
||||
Product? product,
|
||||
double? quantity,
|
||||
}) {
|
||||
CartItemData copyWith({Product? product, double? quantity}) {
|
||||
return CartItemData(
|
||||
product: product ?? this.product,
|
||||
quantity: quantity ?? this.quantity,
|
||||
@@ -101,7 +95,8 @@ class CartState {
|
||||
discountCode: discountCode ?? this.discountCode,
|
||||
discountCodeApplied: discountCodeApplied ?? this.discountCodeApplied,
|
||||
memberTier: memberTier ?? this.memberTier,
|
||||
memberDiscountPercent: memberDiscountPercent ?? this.memberDiscountPercent,
|
||||
memberDiscountPercent:
|
||||
memberDiscountPercent ?? this.memberDiscountPercent,
|
||||
subtotal: subtotal ?? this.subtotal,
|
||||
memberDiscount: memberDiscount ?? this.memberDiscount,
|
||||
shippingFee: shippingFee ?? this.shippingFee,
|
||||
|
||||
@@ -22,10 +22,7 @@ import 'package:worker/features/cart/presentation/providers/cart_state.dart';
|
||||
class CartItemWidget extends ConsumerWidget {
|
||||
final CartItemData item;
|
||||
|
||||
const CartItemWidget({
|
||||
super.key,
|
||||
required this.item,
|
||||
});
|
||||
const CartItemWidget({super.key, required this.item});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
@@ -65,9 +62,7 @@ class CartItemWidget extends ConsumerWidget {
|
||||
height: 80,
|
||||
color: AppColors.grey100,
|
||||
child: const Center(
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
),
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
),
|
||||
),
|
||||
errorWidget: (context, url, error) => Container(
|
||||
@@ -129,9 +124,9 @@ class CartItemWidget extends ConsumerWidget {
|
||||
_QuantityButton(
|
||||
icon: Icons.remove,
|
||||
onPressed: () {
|
||||
ref.read(cartProvider.notifier).decrementQuantity(
|
||||
item.product.productId,
|
||||
);
|
||||
ref
|
||||
.read(cartProvider.notifier)
|
||||
.decrementQuantity(item.product.productId);
|
||||
},
|
||||
),
|
||||
|
||||
@@ -151,9 +146,9 @@ class CartItemWidget extends ConsumerWidget {
|
||||
_QuantityButton(
|
||||
icon: Icons.add,
|
||||
onPressed: () {
|
||||
ref.read(cartProvider.notifier).incrementQuantity(
|
||||
item.product.productId,
|
||||
);
|
||||
ref
|
||||
.read(cartProvider.notifier)
|
||||
.incrementQuantity(item.product.productId);
|
||||
},
|
||||
),
|
||||
|
||||
@@ -184,10 +179,7 @@ class _QuantityButton extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final VoidCallback onPressed;
|
||||
|
||||
const _QuantityButton({
|
||||
required this.icon,
|
||||
required this.onPressed,
|
||||
});
|
||||
const _QuantityButton({required this.icon, required this.onPressed});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -201,11 +193,7 @@ class _QuantityButton extends StatelessWidget {
|
||||
color: AppColors.grey100,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Icon(
|
||||
icon,
|
||||
size: 18,
|
||||
color: AppColors.grey900,
|
||||
),
|
||||
child: Icon(icon, size: 18, color: AppColors.grey900),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -68,8 +68,11 @@ class CheckoutDatePickerField extends HookWidget {
|
||||
: AppColors.grey500.withValues(alpha: 0.6),
|
||||
),
|
||||
),
|
||||
const Icon(Icons.calendar_today,
|
||||
size: 20, color: AppColors.grey500),
|
||||
const Icon(
|
||||
Icons.calendar_today,
|
||||
size: 20,
|
||||
color: AppColors.grey500,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -75,10 +75,7 @@ class CheckoutDropdownField extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
items: items.map((item) {
|
||||
return DropdownMenuItem<String>(
|
||||
value: item,
|
||||
child: Text(item),
|
||||
);
|
||||
return DropdownMenuItem<String>(value: item, child: Text(item));
|
||||
}).toList(),
|
||||
onChanged: onChanged,
|
||||
validator: (value) {
|
||||
|
||||
@@ -114,7 +114,8 @@ class CheckoutSubmitButton extends StatelessWidget {
|
||||
});
|
||||
} else {
|
||||
// Generate order ID (mock - replace with actual from backend)
|
||||
final orderId = 'DH${DateTime.now().millisecondsSinceEpoch.toString().substring(7)}';
|
||||
final orderId =
|
||||
'DH${DateTime.now().millisecondsSinceEpoch.toString().substring(7)}';
|
||||
|
||||
// Show order success message
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
@@ -130,10 +131,7 @@ class CheckoutSubmitButton extends StatelessWidget {
|
||||
if (context.mounted) {
|
||||
context.pushNamed(
|
||||
RouteNames.paymentQr,
|
||||
queryParameters: {
|
||||
'orderId': orderId,
|
||||
'amount': total.toString(),
|
||||
},
|
||||
queryParameters: {'orderId': orderId, 'amount': total.toString()},
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -103,13 +103,7 @@ class DeliveryInformationSection extends HookWidget {
|
||||
label: 'Tỉnh/Thành phố',
|
||||
value: selectedProvince.value,
|
||||
required: true,
|
||||
items: const [
|
||||
'TP.HCM',
|
||||
'Hà Nội',
|
||||
'Đà Nẵng',
|
||||
'Cần Thơ',
|
||||
'Biên Hòa',
|
||||
],
|
||||
items: const ['TP.HCM', 'Hà Nội', 'Đà Nẵng', 'Cần Thơ', 'Biên Hòa'],
|
||||
onChanged: (value) {
|
||||
selectedProvince.value = value;
|
||||
},
|
||||
|
||||
@@ -132,8 +132,9 @@ class InvoiceSection extends HookWidget {
|
||||
return 'Vui lòng nhập email';
|
||||
}
|
||||
if (needsInvoice.value &&
|
||||
!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$')
|
||||
.hasMatch(value!)) {
|
||||
!RegExp(
|
||||
r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$',
|
||||
).hasMatch(value!)) {
|
||||
return 'Email không hợp lệ';
|
||||
}
|
||||
return null;
|
||||
|
||||
@@ -148,8 +148,10 @@ class OrderSummarySection extends StatelessWidget {
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Mã: ${item['sku']}',
|
||||
style:
|
||||
const TextStyle(fontSize: 12, color: AppColors.grey500),
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppColors.grey500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -168,8 +170,9 @@ class OrderSummarySection extends StatelessWidget {
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
_formatCurrency(
|
||||
((item['price'] as int) * (item['quantity'] as int))
|
||||
.toDouble()),
|
||||
((item['price'] as int) * (item['quantity'] as int))
|
||||
.toDouble(),
|
||||
),
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
@@ -184,8 +187,11 @@ class OrderSummarySection extends StatelessWidget {
|
||||
}
|
||||
|
||||
/// Build summary row
|
||||
Widget _buildSummaryRow(String label, double amount,
|
||||
{bool isDiscount = false}) {
|
||||
Widget _buildSummaryRow(
|
||||
String label,
|
||||
double amount, {
|
||||
bool isDiscount = false,
|
||||
}) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
@@ -207,9 +213,6 @@ class OrderSummarySection extends StatelessWidget {
|
||||
|
||||
/// Format currency
|
||||
String _formatCurrency(double amount) {
|
||||
return '${amount.toStringAsFixed(0).replaceAllMapped(
|
||||
RegExp(r'(\d)(?=(\d{3})+(?!\d))'),
|
||||
(Match m) => '${m[1]}.',
|
||||
)}₫';
|
||||
return '${amount.toStringAsFixed(0).replaceAllMapped(RegExp(r'(\d)(?=(\d{3})+(?!\d))'), (Match m) => '${m[1]}.')}₫';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,10 +14,7 @@ import 'package:worker/core/theme/colors.dart';
|
||||
class PaymentMethodSection extends HookWidget {
|
||||
final ValueNotifier<String> paymentMethod;
|
||||
|
||||
const PaymentMethodSection({
|
||||
super.key,
|
||||
required this.paymentMethod,
|
||||
});
|
||||
const PaymentMethodSection({super.key, required this.paymentMethod});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -72,13 +69,17 @@ class PaymentMethodSection extends HookWidget {
|
||||
Text(
|
||||
'Chuyển khoản ngân hàng',
|
||||
style: TextStyle(
|
||||
fontSize: 15, fontWeight: FontWeight.w500),
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 4),
|
||||
Text(
|
||||
'Thanh toán qua chuyển khoản',
|
||||
style:
|
||||
TextStyle(fontSize: 13, color: AppColors.grey500),
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: AppColors.grey500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -112,13 +113,17 @@ class PaymentMethodSection extends HookWidget {
|
||||
Text(
|
||||
'Thanh toán khi nhận hàng (COD)',
|
||||
style: TextStyle(
|
||||
fontSize: 15, fontWeight: FontWeight.w500),
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 4),
|
||||
Text(
|
||||
'Thanh toán bằng tiền mặt khi nhận hàng',
|
||||
style:
|
||||
TextStyle(fontSize: 13, color: AppColors.grey500),
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: AppColors.grey500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -14,10 +14,7 @@ import 'package:worker/core/theme/colors.dart';
|
||||
class PriceNegotiationSection extends HookWidget {
|
||||
final ValueNotifier<bool> needsNegotiation;
|
||||
|
||||
const PriceNegotiationSection({
|
||||
super.key,
|
||||
required this.needsNegotiation,
|
||||
});
|
||||
const PriceNegotiationSection({super.key, required this.needsNegotiation});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
||||
Reference in New Issue
Block a user