add auth, format

This commit is contained in:
Phuoc Nguyen
2025-11-07 11:52:06 +07:00
parent 24a8508fce
commit 3803bd26e0
173 changed files with 8505 additions and 7116 deletions

View File

@@ -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

View File

@@ -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),
),
),
),

View File

@@ -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),

View File

@@ -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);
}
}

View File

@@ -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,

View File

@@ -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),
),
);
}

View File

@@ -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,
),
],
),
),

View File

@@ -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) {

View File

@@ -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()},
);
}
});

View File

@@ -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;
},

View File

@@ -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;

View File

@@ -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]}.')}';
}
}

View File

@@ -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,
),
),
],
),

View File

@@ -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) {