463 lines
16 KiB
Dart
463 lines
16 KiB
Dart
/// Checkout Page
|
|
///
|
|
/// Complete checkout flow with delivery info, invoice options, payment methods.
|
|
/// Features:
|
|
/// - Delivery information form with province/ward dropdowns
|
|
/// - Invoice toggle section (checkbox reveals invoice fields)
|
|
/// - Payment method selection (bank transfer vs COD)
|
|
/// - Order summary with items list
|
|
/// - Price negotiation option (hides payment, changes button)
|
|
/// - Form validation and submission
|
|
library;
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
|
import 'package:go_router/go_router.dart';
|
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|
|
|
import 'package:worker/core/constants/ui_constants.dart';
|
|
import 'package:worker/core/theme/colors.dart';
|
|
import 'package:worker/features/account/domain/entities/address.dart';
|
|
import 'package:worker/features/cart/presentation/providers/cart_state.dart';
|
|
import 'package:worker/features/cart/presentation/widgets/checkout_submit_button.dart';
|
|
import 'package:worker/features/cart/presentation/widgets/delivery_information_section.dart';
|
|
import 'package:worker/features/cart/presentation/widgets/invoice_section.dart';
|
|
import 'package:worker/features/cart/presentation/widgets/order_summary_section.dart';
|
|
import 'package:worker/features/cart/presentation/widgets/payment_method_section.dart';
|
|
import 'package:worker/features/cart/presentation/widgets/price_negotiation_section.dart';
|
|
import 'package:worker/features/orders/presentation/providers/order_status_provider.dart';
|
|
import 'package:worker/features/orders/presentation/providers/payment_terms_provider.dart';
|
|
|
|
/// Checkout Page
|
|
///
|
|
/// Full checkout flow for placing orders.
|
|
class CheckoutPage extends HookConsumerWidget {
|
|
const CheckoutPage({
|
|
super.key,
|
|
this.checkoutData,
|
|
});
|
|
|
|
final Map<String, dynamic>? checkoutData;
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
// Form key for validation
|
|
final formKey = useMemoized(() => GlobalKey<FormState>());
|
|
|
|
// Delivery information
|
|
final notesController = useTextEditingController();
|
|
final selectedPickupDate = useState<DateTime?>(null);
|
|
final selectedAddress = useState<Address?>(null);
|
|
|
|
// Invoice section
|
|
final needsInvoice = useState<bool>(false);
|
|
|
|
// Payment method (will be set to first payment term name from API)
|
|
final paymentMethod = useState<String>('');
|
|
|
|
// Price negotiation (ignore_pricing_rule in API)
|
|
final ignorePricingRule = useState<bool>(false);
|
|
|
|
// Contract request (contract_request in API)
|
|
final contractRequest = useState<bool>(false);
|
|
|
|
// Watch API provider for payment terms
|
|
final paymentTermsListAsync = ref.watch(paymentTermsListProvider);
|
|
|
|
// Get CartItemData from navigation
|
|
final cartItemsData = checkoutData?['cartItems'] as List<dynamic>? ?? [];
|
|
|
|
// Convert CartItemData to Map format for OrderSummarySection
|
|
// Use all data directly from cart (no API calls needed)
|
|
final checkoutItems = cartItemsData.map((itemData) {
|
|
final cartItem = itemData as CartItemData;
|
|
return {
|
|
'id': cartItem.product.productId,
|
|
'name': cartItem.product.name,
|
|
'sku': cartItem.product.erpnextItemCode ?? cartItem.product.productId,
|
|
'quantity': cartItem.quantity,
|
|
'quantityConverted': cartItem.quantityConverted,
|
|
'boxes': cartItem.boxes,
|
|
'price': cartItem.product.basePrice,
|
|
'image': cartItem.product.images.isNotEmpty
|
|
? cartItem.product.images.first
|
|
: null,
|
|
};
|
|
}).toList();
|
|
|
|
// Calculate totals from cart data
|
|
final subtotal = checkoutItems.fold<double>(
|
|
0.0,
|
|
(sum, item) => sum + (item['price'] as double) * (item['quantityConverted'] as double),
|
|
);
|
|
|
|
// TODO: Fetch member discount from user profile API
|
|
const memberDiscountPercent = 0.0; // Temporarily disabled (was 15.0 for Diamond tier)
|
|
final memberDiscount = subtotal * (memberDiscountPercent / 100);
|
|
|
|
// TODO: Fetch shipping fee from API based on address
|
|
const shipping = 0.0; // Free shipping (temporary)
|
|
|
|
final total = subtotal - memberDiscount + shipping;
|
|
|
|
return Scaffold(
|
|
backgroundColor: const Color(0xFFF4F6F8),
|
|
appBar: AppBar(
|
|
backgroundColor: Colors.white,
|
|
elevation: 0,
|
|
leading: IconButton(
|
|
icon: const FaIcon(
|
|
FontAwesomeIcons.arrowLeft,
|
|
color: Colors.black,
|
|
size: 20,
|
|
),
|
|
onPressed: () => context.pop(),
|
|
),
|
|
title: const Text(
|
|
'Thanh toán',
|
|
style: TextStyle(
|
|
color: Colors.black,
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
centerTitle: false,
|
|
actions: const [SizedBox(width: AppSpacing.sm)],
|
|
),
|
|
body: Form(
|
|
key: formKey,
|
|
child: SingleChildScrollView(
|
|
child: Column(
|
|
children: [
|
|
const SizedBox(height: AppSpacing.md),
|
|
|
|
// Delivery Information Section
|
|
DeliveryInformationSection(
|
|
notesController: notesController,
|
|
selectedPickupDate: selectedPickupDate,
|
|
selectedAddress: selectedAddress,
|
|
),
|
|
|
|
const SizedBox(height: AppSpacing.md),
|
|
|
|
// Invoice Section
|
|
InvoiceSection(needsInvoice: needsInvoice),
|
|
|
|
const SizedBox(height: AppSpacing.md),
|
|
|
|
// Payment Method Section (hidden if price negotiation is checked)
|
|
if (!ignorePricingRule.value)
|
|
paymentTermsListAsync.when(
|
|
data: (paymentTerms) {
|
|
// Set default payment method to first term if not set
|
|
if (paymentMethod.value.isEmpty && paymentTerms.isNotEmpty) {
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
paymentMethod.value = paymentTerms.first.name;
|
|
});
|
|
}
|
|
return PaymentMethodSection(
|
|
paymentMethod: paymentMethod,
|
|
paymentTerms: paymentTerms,
|
|
);
|
|
},
|
|
loading: () => Container(
|
|
margin: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
|
|
padding: const EdgeInsets.all(AppSpacing.md),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(AppRadius.card),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withValues(alpha: 0.05),
|
|
blurRadius: 8,
|
|
offset: const Offset(0, 2),
|
|
),
|
|
],
|
|
),
|
|
child: const Center(
|
|
child: CircularProgressIndicator(),
|
|
),
|
|
),
|
|
error: (error, stack) => Container(
|
|
margin: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
|
|
padding: const EdgeInsets.all(AppSpacing.md),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(AppRadius.card),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withValues(alpha: 0.05),
|
|
blurRadius: 8,
|
|
offset: const Offset(0, 2),
|
|
),
|
|
],
|
|
),
|
|
child: Column(
|
|
children: [
|
|
const Icon(
|
|
FontAwesomeIcons.triangleExclamation,
|
|
color: AppColors.danger,
|
|
size: 32,
|
|
),
|
|
const SizedBox(height: 12),
|
|
Text(
|
|
'Không thể tải phương thức thanh toán',
|
|
style: const TextStyle(
|
|
fontSize: 14,
|
|
color: AppColors.grey500,
|
|
),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
const SizedBox(height: 8),
|
|
TextButton(
|
|
onPressed: () {
|
|
ref.invalidate(paymentTermsListProvider);
|
|
},
|
|
child: const Text('Thử lại'),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
|
|
if (!ignorePricingRule.value)
|
|
const SizedBox(height: AppSpacing.md),
|
|
|
|
// Discount Code Section
|
|
_buildDiscountCodeSection(),
|
|
|
|
const SizedBox(height: AppSpacing.md),
|
|
|
|
// Order Summary Section
|
|
OrderSummarySection(
|
|
cartItems: checkoutItems,
|
|
subtotal: subtotal,
|
|
discount: memberDiscount,
|
|
shipping: shipping,
|
|
total: total,
|
|
),
|
|
|
|
const SizedBox(height: AppSpacing.md),
|
|
|
|
// Price Negotiation Section
|
|
PriceNegotiationSection(ignorePricingRule: ignorePricingRule),
|
|
|
|
const SizedBox(height: AppSpacing.md),
|
|
|
|
|
|
Container(
|
|
margin: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
|
|
padding: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFFFFF8E1),
|
|
borderRadius: BorderRadius.circular(AppRadius.card),
|
|
border: Border.all(color: const Color(0xFFFFD54F)),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Checkbox(
|
|
value: contractRequest.value,
|
|
onChanged: (value) {
|
|
contractRequest.value = value ?? false;
|
|
},
|
|
activeColor: AppColors.warning,
|
|
),
|
|
const Expanded(
|
|
child: Text(
|
|
'Yêu cầu hợp đồng',
|
|
style: TextStyle(
|
|
fontSize: 15,
|
|
fontWeight: FontWeight.w600,
|
|
color: Color(0xFF212121),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
|
|
const SizedBox(height: AppSpacing.md),
|
|
|
|
// Terms and Conditions
|
|
const Padding(
|
|
padding: EdgeInsets.symmetric(horizontal: AppSpacing.md),
|
|
child: Text.rich(
|
|
TextSpan(
|
|
text: 'Bằng cách đặt hàng, bạn đồng ý với ',
|
|
style: TextStyle(
|
|
fontSize: 14,
|
|
color: Color(0xFF6B7280),
|
|
),
|
|
children: [
|
|
TextSpan(
|
|
text: 'Điều khoản & Điều kiện',
|
|
style: TextStyle(
|
|
color: AppColors.primaryBlue,
|
|
decoration: TextDecoration.underline,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: AppSpacing.md),
|
|
|
|
// Place Order Button
|
|
CheckoutSubmitButton(
|
|
formKey: formKey,
|
|
ignorePricingRule: ignorePricingRule.value,
|
|
contractRequest: contractRequest.value,
|
|
needsInvoice: needsInvoice.value,
|
|
selectedAddress: selectedAddress.value,
|
|
paymentMethod: paymentMethod.value,
|
|
total: total,
|
|
cartItems: checkoutItems,
|
|
notes: notesController.text.trim().isEmpty ? null : notesController.text.trim(),
|
|
),
|
|
|
|
const SizedBox(height: AppSpacing.lg),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
/// Build Discount Code Section (Card 4 from HTML)
|
|
Widget _buildDiscountCodeSection() {
|
|
return Container(
|
|
margin: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
|
|
padding: const EdgeInsets.all(AppSpacing.md),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(AppRadius.card),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withValues(alpha: 0.05),
|
|
blurRadius: 8,
|
|
offset: const Offset(0, 2),
|
|
),
|
|
],
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// Section Title
|
|
Row(
|
|
children: [
|
|
Icon(
|
|
FontAwesomeIcons.ticket,
|
|
color: AppColors.primaryBlue,
|
|
size: 20,
|
|
),
|
|
const SizedBox(width: 8),
|
|
const Text(
|
|
'Mã giảm giá',
|
|
style: TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.bold,
|
|
color: Color(0xFF212121),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
|
|
const SizedBox(height: 12),
|
|
|
|
// Input field with Apply button
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: TextField(
|
|
decoration: InputDecoration(
|
|
hintText: 'Nhập mã giảm giá',
|
|
border: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(8),
|
|
borderSide: const BorderSide(color: Color(0xFFD1D5DB)),
|
|
),
|
|
enabledBorder: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(8),
|
|
borderSide: const BorderSide(color: Color(0xFFD1D5DB)),
|
|
),
|
|
focusedBorder: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(8),
|
|
borderSide: const BorderSide(
|
|
color: AppColors.primaryBlue,
|
|
width: 2,
|
|
),
|
|
),
|
|
contentPadding: const EdgeInsets.symmetric(
|
|
horizontal: 16,
|
|
vertical: 12,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 8),
|
|
ElevatedButton(
|
|
onPressed: () {
|
|
// TODO: Apply discount code
|
|
},
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: AppColors.primaryBlue,
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 24,
|
|
vertical: 12,
|
|
),
|
|
shape: RoundedRectangleBorder(
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
elevation: 0,
|
|
),
|
|
child: const Text(
|
|
'Áp dụng',
|
|
style: TextStyle(
|
|
color: Colors.white,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
|
|
const SizedBox(height: 12),
|
|
|
|
// Success banner (Diamond discount)
|
|
Container(
|
|
padding: const EdgeInsets.all(12),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFFF0FDF4),
|
|
border: Border.all(color: const Color(0xFFBBF7D0)),
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Icon(
|
|
FontAwesomeIcons.circleCheck,
|
|
color: AppColors.success,
|
|
size: 18,
|
|
),
|
|
const SizedBox(width: 8),
|
|
const Expanded(
|
|
child: Text(
|
|
'Bạn được giảm 15% (hạng Diamond)',
|
|
style: TextStyle(
|
|
fontSize: 14,
|
|
color: Color(0xFF166534),
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|