From fc4711a18eb91f3393e5330925a232084274f9b3 Mon Sep 17 00:00:00 2001 From: Phuoc Nguyen Date: Tue, 18 Nov 2025 17:59:27 +0700 Subject: [PATCH] fix --- lib/core/router/app_router.dart | 18 +- .../presentation/pages/addresses_page.dart | 191 ++++++++++-- .../presentation/widgets/address_card.dart | 26 +- .../presentation/pages/checkout_page.dart | 43 +-- .../widgets/checkout_submit_button.dart | 41 +-- .../widgets/delivery_information_section.dart | 291 +++++++++++------- .../presentation/widgets/invoice_section.dart | 241 +++++++++------ pubspec.yaml | 2 +- 8 files changed, 568 insertions(+), 285 deletions(-) diff --git a/lib/core/router/app_router.dart b/lib/core/router/app_router.dart index 1029944..ffa3fa6 100644 --- a/lib/core/router/app_router.dart +++ b/lib/core/router/app_router.dart @@ -62,13 +62,18 @@ final routerProvider = Provider((ref) { final isLoggedIn = authState.value != null; final isOnSplashPage = state.matchedLocation == RouteNames.splash; final isOnLoginPage = state.matchedLocation == RouteNames.login; - final isOnForgotPasswordPage = state.matchedLocation == RouteNames.forgotPassword; + final isOnForgotPasswordPage = + state.matchedLocation == RouteNames.forgotPassword; final isOnRegisterPage = state.matchedLocation == RouteNames.register; final isOnBusinessUnitPage = state.matchedLocation == RouteNames.businessUnitSelection; final isOnOtpPage = state.matchedLocation == RouteNames.otpVerification; final isOnAuthPage = - isOnLoginPage || isOnForgotPasswordPage || isOnRegisterPage || isOnBusinessUnitPage || isOnOtpPage; + isOnLoginPage || + isOnForgotPasswordPage || + isOnRegisterPage || + isOnBusinessUnitPage || + isOnOtpPage; // While loading auth state, show splash screen if (isLoading) { @@ -367,8 +372,13 @@ final routerProvider = Provider((ref) { GoRoute( path: RouteNames.addresses, name: RouteNames.addresses, - pageBuilder: (context, state) => - MaterialPage(key: state.pageKey, child: const AddressesPage()), + pageBuilder: (context, state) { + final extra = state.extra as Map?; + return MaterialPage( + key: state.pageKey, + child: AddressesPage(extra: extra), + ); + }, ), // Address Form Route (Create/Edit) diff --git a/lib/features/account/presentation/pages/addresses_page.dart b/lib/features/account/presentation/pages/addresses_page.dart index 541025f..fd10088 100644 --- a/lib/features/account/presentation/pages/addresses_page.dart +++ b/lib/features/account/presentation/pages/addresses_page.dart @@ -10,6 +10,7 @@ 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'; @@ -23,11 +24,21 @@ import 'package:worker/features/account/presentation/widgets/address_card.dart'; /// Addresses Page /// /// Page for managing saved delivery addresses. -class AddressesPage extends ConsumerWidget { - const AddressesPage({super.key}); +/// Supports selection mode for returning selected address. +class AddressesPage extends HookConsumerWidget { + const AddressesPage({super.key, this.extra}); + + final Map? extra; @override Widget build(BuildContext context, WidgetRef ref) { + // Check if in selection mode + final selectMode = extra?['selectMode'] == true; + final currentAddress = extra?['currentAddress'] as Address?; + + // Selected address state (for selection mode) + final selectedAddress = useState(currentAddress); + // Watch addresses from API final addressesAsync = ref.watch(addressesProvider); @@ -37,18 +48,26 @@ class AddressesPage extends ConsumerWidget { backgroundColor: AppColors.white, elevation: AppBarSpecs.elevation, leading: IconButton( - icon: const FaIcon(FontAwesomeIcons.arrowLeft, color: Colors.black, size: 20), + icon: const FaIcon( + FontAwesomeIcons.arrowLeft, + color: Colors.black, + size: 20, + ), onPressed: () => context.pop(), ), - title: const Text( - 'Địa chỉ của bạn', - style: TextStyle(color: Colors.black), + title: Text( + selectMode ? 'Chọn địa chỉ' : 'Địa chỉ của bạn', + style: const TextStyle(color: Colors.black), ), foregroundColor: AppColors.grey900, centerTitle: false, actions: [ IconButton( - icon: const FaIcon(FontAwesomeIcons.circleInfo, color: Colors.black, size: 20), + icon: const FaIcon( + FontAwesomeIcons.circleInfo, + color: Colors.black, + size: 20, + ), onPressed: () { _showInfoDialog(context); }, @@ -74,6 +93,37 @@ class AddressesPage extends ConsumerWidget { const SizedBox(height: AppSpacing.md), itemBuilder: (context, index) { final address = addresses[index]; + final isSelected = + selectedAddress.value?.name == address.name; + + // In selection mode, show radio button + if (selectMode) { + return AddressCard( + name: address.addressTitle, + phone: address.phone, + address: address.fullAddress, + isDefault: address.isDefault, + showRadio: true, + isSelected: isSelected, + onRadioTap: () { + selectedAddress.value = address; + }, + // Keep edit/delete actions in selection mode + onEdit: () { + context.push( + RouteNames.addressForm, + extra: address, + ); + }, + onDelete: () { + _showDeleteConfirmation(context, ref, address); + }, + onSetDefault: + null, // Hide set default in selection mode + ); + } + + // Normal mode - show all actions return AddressCard( name: address.addressTitle, phone: address.phone, @@ -97,31 +147,107 @@ class AddressesPage extends ConsumerWidget { ), ), - // Add New Address Button + // Bottom Buttons Padding( padding: const EdgeInsets.all(AppSpacing.md), - child: SizedBox( - width: double.infinity, - child: ElevatedButton.icon( - onPressed: () { - context.push(RouteNames.addressForm); - }, - icon: const FaIcon(FontAwesomeIcons.plus, size: 18), - label: const Text( - 'Thêm địa chỉ mới', - style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600), - ), - style: ElevatedButton.styleFrom( - backgroundColor: AppColors.primaryBlue, - foregroundColor: Colors.white, - padding: const EdgeInsets.symmetric(vertical: 14), - elevation: 0, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(AppRadius.button), + child: selectMode + ? Row( + children: [ + // Add New Address Button (Selection Mode) + Expanded( + child: OutlinedButton.icon( + onPressed: () { + context.push(RouteNames.addressForm); + }, + icon: const FaIcon(FontAwesomeIcons.plus, size: 16), + label: const Text( + 'Thêm mới', + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.w600, + ), + ), + style: OutlinedButton.styleFrom( + foregroundColor: AppColors.primaryBlue, + side: const BorderSide( + color: AppColors.primaryBlue, + width: 1.5, + ), + padding: const EdgeInsets.symmetric(vertical: 14), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + AppRadius.button, + ), + ), + ), + ), + ), + const SizedBox(width: AppSpacing.md), + // Select Address Button + Expanded( + flex: 2, + child: ElevatedButton.icon( + onPressed: selectedAddress.value == null + ? null + : () { + // Return selected address without setting as default + context.pop(selectedAddress.value); + }, + icon: const FaIcon( + FontAwesomeIcons.check, + size: 16, + ), + label: const Text( + 'Chọn địa chỉ này', + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.w600, + ), + ), + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.primaryBlue, + foregroundColor: Colors.white, + disabledBackgroundColor: AppColors.grey100, + disabledForegroundColor: AppColors.grey500, + padding: const EdgeInsets.symmetric(vertical: 14), + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + AppRadius.button, + ), + ), + ), + ), + ), + ], + ) + : SizedBox( + width: double.infinity, + child: ElevatedButton.icon( + onPressed: () { + context.push(RouteNames.addressForm); + }, + icon: const FaIcon(FontAwesomeIcons.plus, size: 18), + label: const Text( + 'Thêm địa chỉ mới', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.primaryBlue, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 14), + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + AppRadius.button, + ), + ), + ), + ), ), - ), - ), - ), ), ], ), @@ -166,7 +292,6 @@ class AddressesPage extends ConsumerWidget { ), ), ), - ); } @@ -224,7 +349,11 @@ class AddressesPage extends ConsumerWidget { } /// Set address as default - void _setDefaultAddress(BuildContext context, WidgetRef ref, Address address) { + void _setDefaultAddress( + BuildContext context, + WidgetRef ref, + Address address, + ) { ref.read(addressesProvider.notifier).setDefaultAddress(address.name); ScaffoldMessenger.of(context).showSnackBar( diff --git a/lib/features/account/presentation/widgets/address_card.dart b/lib/features/account/presentation/widgets/address_card.dart index f171b35..68502ac 100644 --- a/lib/features/account/presentation/widgets/address_card.dart +++ b/lib/features/account/presentation/widgets/address_card.dart @@ -12,6 +12,7 @@ import 'package:worker/core/theme/colors.dart'; /// /// Shows address details with name, phone, address text, default badge, /// and action buttons (edit/delete). +/// Supports selection mode with radio button. class AddressCard extends StatelessWidget { final String name; final String phone; @@ -20,6 +21,9 @@ class AddressCard extends StatelessWidget { final VoidCallback? onEdit; final VoidCallback? onDelete; final VoidCallback? onSetDefault; + final bool showRadio; + final bool isSelected; + final VoidCallback? onRadioTap; const AddressCard({ super.key, @@ -30,6 +34,9 @@ class AddressCard extends StatelessWidget { this.onEdit, this.onDelete, this.onSetDefault, + this.showRadio = false, + this.isSelected = false, + this.onRadioTap, }); @override @@ -61,6 +68,21 @@ class AddressCard extends StatelessWidget { child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ + // Radio Button (Selection Mode) + if (showRadio) + GestureDetector( + onTap: onRadioTap, + child: Padding( + padding: const EdgeInsets.only(right: 12, top: 2), + child: Radio( + value: true, + groupValue: isSelected, + onChanged: (_) => onRadioTap?.call(), + activeColor: AppColors.primaryBlue, + ), + ), + ), + // Address Content Expanded( child: Column( @@ -111,7 +133,9 @@ class AddressCard extends StatelessWidget { ), decoration: BoxDecoration( border: Border.all( - color: AppColors.primaryBlue.withValues(alpha: 0.3), + color: AppColors.primaryBlue.withValues( + alpha: 0.3, + ), ), borderRadius: BorderRadius.circular(4), ), diff --git a/lib/features/cart/presentation/pages/checkout_page.dart b/lib/features/cart/presentation/pages/checkout_page.dart index b5c60d9..6206dee 100644 --- a/lib/features/cart/presentation/pages/checkout_page.dart +++ b/lib/features/cart/presentation/pages/checkout_page.dart @@ -17,6 +17,7 @@ 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/features/account/domain/entities/address.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'; @@ -35,23 +36,13 @@ class CheckoutPage extends HookConsumerWidget { // Form key for validation final formKey = useMemoized(() => GlobalKey()); - // Delivery information controllers - final nameController = useTextEditingController(text: 'Hoàng Minh Hiệp'); - final phoneController = useTextEditingController(text: '0347302911'); - final addressController = useTextEditingController(); + // Delivery information final notesController = useTextEditingController(); - - // Dropdown selections - final selectedProvince = useState('TP.HCM'); - final selectedWard = useState('Quận 1'); final selectedPickupDate = useState(null); + final selectedAddress = useState(null); // Invoice section final needsInvoice = useState(false); - final companyNameController = useTextEditingController(); - final taxIdController = useTextEditingController(); - final companyAddressController = useTextEditingController(); - final companyEmailController = useTextEditingController(); // Payment method final paymentMethod = useState('full_payment'); @@ -96,7 +87,11 @@ class CheckoutPage extends HookConsumerWidget { backgroundColor: Colors.white, elevation: 0, leading: IconButton( - icon: const FaIcon(FontAwesomeIcons.arrowLeft, color: Colors.black, size: 20), + icon: const FaIcon( + FontAwesomeIcons.arrowLeft, + color: Colors.black, + size: 20, + ), onPressed: () => context.pop(), ), title: const Text( @@ -119,25 +114,15 @@ class CheckoutPage extends HookConsumerWidget { // Delivery Information Section DeliveryInformationSection( - nameController: nameController, - phoneController: phoneController, - addressController: addressController, notesController: notesController, - selectedProvince: selectedProvince, - selectedWard: selectedWard, selectedPickupDate: selectedPickupDate, + selectedAddress: selectedAddress, ), const SizedBox(height: AppSpacing.md), // Invoice Section - InvoiceSection( - needsInvoice: needsInvoice, - companyNameController: companyNameController, - taxIdController: taxIdController, - companyAddressController: companyAddressController, - companyEmailController: companyEmailController, - ), + InvoiceSection(needsInvoice: needsInvoice), const SizedBox(height: AppSpacing.md), @@ -169,14 +154,8 @@ class CheckoutPage extends HookConsumerWidget { formKey: formKey, needsNegotiation: needsNegotiation.value, needsInvoice: needsInvoice.value, - name: nameController.text, - phone: phoneController.text, - address: addressController.text, - province: selectedProvince.value, - ward: selectedWard.value, + selectedAddress: selectedAddress.value, paymentMethod: paymentMethod.value, - companyName: companyNameController.text, - taxId: taxIdController.text, total: total, ), diff --git a/lib/features/cart/presentation/widgets/checkout_submit_button.dart b/lib/features/cart/presentation/widgets/checkout_submit_button.dart index 45607f9..68de7d9 100644 --- a/lib/features/cart/presentation/widgets/checkout_submit_button.dart +++ b/lib/features/cart/presentation/widgets/checkout_submit_button.dart @@ -9,40 +9,29 @@ import 'package:go_router/go_router.dart'; import 'package:worker/core/constants/ui_constants.dart'; import 'package:worker/core/router/app_router.dart'; import 'package:worker/core/theme/colors.dart'; +import 'package:worker/features/account/domain/entities/address.dart'; /// Checkout Submit Button /// /// Button that changes based on negotiation checkbox state. class CheckoutSubmitButton extends StatelessWidget { - final GlobalKey formKey; - final bool needsNegotiation; - final bool needsInvoice; - final String name; - final String phone; - final String address; - final String? province; - final String? ward; - final String paymentMethod; - final String companyName; - final String taxId; - final double total; - const CheckoutSubmitButton({ super.key, required this.formKey, required this.needsNegotiation, required this.needsInvoice, - required this.name, - required this.phone, - required this.address, - required this.province, - required this.ward, + required this.selectedAddress, required this.paymentMethod, - required this.companyName, - required this.taxId, required this.total, }); + final GlobalKey formKey; + final bool needsNegotiation; + final bool needsInvoice; + final Address? selectedAddress; + final String paymentMethod; + final double total; + @override Widget build(BuildContext context) { return Padding( @@ -63,6 +52,18 @@ class CheckoutSubmitButton extends StatelessWidget { width: double.infinity, child: ElevatedButton( onPressed: () { + // Validate address is selected + if (selectedAddress == null) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Vui lòng chọn địa chỉ giao hàng'), + backgroundColor: AppColors.danger, + duration: Duration(seconds: 2), + ), + ); + return; + } + if (formKey.currentState?.validate() ?? false) { _handlePlaceOrder(context); } diff --git a/lib/features/cart/presentation/widgets/delivery_information_section.dart b/lib/features/cart/presentation/widgets/delivery_information_section.dart index 56b2736..8c69e21 100644 --- a/lib/features/cart/presentation/widgets/delivery_information_section.dart +++ b/lib/features/cart/presentation/widgets/delivery_information_section.dart @@ -1,40 +1,55 @@ /// Delivery Information Section Widget /// -/// Form section for delivery details including name, phone, address, pickup date. +/// Shows delivery address selection and pickup date/notes. 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/account/presentation/providers/address_provider.dart'; import 'package:worker/features/cart/presentation/widgets/checkout_date_picker_field.dart'; -import 'package:worker/features/cart/presentation/widgets/checkout_dropdown_field.dart'; import 'package:worker/features/cart/presentation/widgets/checkout_text_field.dart'; /// Delivery Information Section /// -/// Collects delivery details from the user with validation. -class DeliveryInformationSection extends HookWidget { - final TextEditingController nameController; - final TextEditingController phoneController; - final TextEditingController addressController; - final TextEditingController notesController; - final ValueNotifier selectedProvince; - final ValueNotifier selectedWard; - final ValueNotifier selectedPickupDate; - +/// Shows default address selection and delivery details. +/// Matches HTML design from checkout.html. +class DeliveryInformationSection extends HookConsumerWidget { const DeliveryInformationSection({ super.key, - required this.nameController, - required this.phoneController, - required this.addressController, required this.notesController, - required this.selectedProvince, - required this.selectedWard, required this.selectedPickupDate, + required this.selectedAddress, }); + final TextEditingController notesController; + final ValueNotifier selectedPickupDate; + final ValueNotifier selectedAddress; + @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { + // Watch the default address + final defaultAddr = ref.watch(defaultAddressProvider); + + // Initialize selected address with default address when it loads + // Only update if user hasn't manually selected a different address + useEffect(() { + if (defaultAddr != null && selectedAddress.value == null) { + // Use Future.microtask to avoid setState during build + Future.microtask(() { + if (selectedAddress.value == null) { + selectedAddress.value = defaultAddr; + } + }); + } + return null; + }, [defaultAddr]); // Watch defaultAddr changes + return Container( margin: const EdgeInsets.symmetric(horizontal: AppSpacing.md), padding: const EdgeInsets.all(AppSpacing.md), @@ -53,104 +68,166 @@ class DeliveryInformationSection extends HookWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ // Section Title - const Text( - 'Thông tin giao hàng', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: Color(0xFF212121), - ), - ), - - const SizedBox(height: AppSpacing.lg), - - // Name Field - CheckoutTextField( - label: 'Họ và tên người nhận', - controller: nameController, - required: true, - validator: (value) { - if (value == null || value.isEmpty) { - return 'Vui lòng nhập họ tên'; - } - return null; - }, - ), - - const SizedBox(height: AppSpacing.md), - - // Phone Field - CheckoutTextField( - label: 'Số điện thoại', - controller: phoneController, - required: true, - keyboardType: TextInputType.phone, - validator: (value) { - if (value == null || value.isEmpty) { - return 'Vui lòng nhập số điện thoại'; - } - if (!RegExp(r'^0\d{9}$').hasMatch(value)) { - return 'Số điện thoại không hợp lệ'; - } - return null; - }, - ), - - const SizedBox(height: AppSpacing.md), - - // Province Dropdown - CheckoutDropdownField( - 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'], - onChanged: (value) { - selectedProvince.value = value; - }, - ), - - const SizedBox(height: AppSpacing.md), - - // Ward Dropdown - CheckoutDropdownField( - label: 'Quận/Huyện', - value: selectedWard.value, - required: true, - items: const [ - 'Quận 1', - 'Quận 2', - 'Quận 3', - 'Quận 4', - 'Quận 5', - 'Thủ Đức', + Row( + children: [ + const FaIcon( + FontAwesomeIcons.truck, + color: AppColors.primaryBlue, + size: 16, + ), + const SizedBox(width: AppSpacing.sm), + const Text( + 'Thông tin giao hàng', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: Color(0xFF212121), + ), + ), ], - onChanged: (value) { - selectedWard.value = value; - }, ), const SizedBox(height: AppSpacing.md), - // Specific Address - CheckoutTextField( - label: 'Địa chỉ cụ thể', - controller: addressController, - required: true, - maxLines: 2, - hintText: 'Số nhà, tên đường, phường/xã', - validator: (value) { - if (value == null || value.isEmpty) { - return 'Vui lòng nhập địa chỉ cụ thể'; - } - return null; - }, + // Address Selection + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Địa chỉ nhận hàng', + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w500, + color: Color(0xFF424242), + ), + ), + const SizedBox(height: AppSpacing.sm), + + // Address Card + if (selectedAddress.value != null) + InkWell( + onTap: () async { + // Navigate to address selection and wait for result + final result = await context.push
( + '/account/addresses', + extra: { + 'selectMode': true, + 'currentAddress': selectedAddress.value, + }, + ); + + // Update selected address if user picked one + if (result != null) { + selectedAddress.value = result; + } + }, + borderRadius: BorderRadius.circular(AppRadius.sm), + child: Container( + padding: const EdgeInsets.all(AppSpacing.sm), + decoration: BoxDecoration( + border: Border.all(color: const Color(0xFFE0E0E0)), + borderRadius: BorderRadius.circular(AppRadius.sm), + ), + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Name + Text( + selectedAddress.value!.addressTitle, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: Color(0xFF212121), + ), + ), + const SizedBox(height: 4), + + // Phone + Text( + selectedAddress.value!.phone, + style: const TextStyle( + fontSize: 12, + color: Color(0xFF757575), + ), + ), + const SizedBox(height: 2), + + // Address + Text( + selectedAddress.value!.fullAddress, + style: const TextStyle( + fontSize: 12, + color: Color(0xFF757575), + ), + ), + ], + ), + ), + const SizedBox(width: AppSpacing.sm), + const FaIcon( + FontAwesomeIcons.chevronRight, + size: 14, + color: Color(0xFF9E9E9E), + ), + ], + ), + ), + ) + else + // No address selected - show add button + InkWell( + onTap: () async { + final result = await context.push
( + '/account/addresses', + extra: {'selectMode': true, 'currentAddress': null}, + ); + + if (result != null) { + selectedAddress.value = result; + } + }, + borderRadius: BorderRadius.circular(AppRadius.sm), + child: Container( + padding: const EdgeInsets.all(AppSpacing.md), + decoration: BoxDecoration( + border: Border.all( + color: AppColors.primaryBlue, + style: BorderStyle.solid, + ), + borderRadius: BorderRadius.circular(AppRadius.sm), + ), + child: const Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + FaIcon( + FontAwesomeIcons.plus, + size: 14, + color: AppColors.primaryBlue, + ), + SizedBox(width: AppSpacing.sm), + Text( + 'Thêm địa chỉ giao hàng', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: AppColors.primaryBlue, + ), + ), + ], + ), + ), + ), + ], ), const SizedBox(height: AppSpacing.md), // Pickup Date CheckoutDatePickerField( - label: 'Ngày nhận hàng mong muốn', + label: 'Ngày lấy hàng', selectedDate: selectedPickupDate, ), @@ -161,8 +238,8 @@ class DeliveryInformationSection extends HookWidget { label: 'Ghi chú', controller: notesController, required: false, - maxLines: 3, - hintText: 'Ghi chú thêm cho đơn hàng (không bắt buộc)', + maxLines: 2, + hintText: 'Ví dụ: Thời gian yêu cầu giao hàng, lưu ý đặc biệt...', ), ], ), diff --git a/lib/features/cart/presentation/widgets/invoice_section.dart b/lib/features/cart/presentation/widgets/invoice_section.dart index 074315c..29284ab 100644 --- a/lib/features/cart/presentation/widgets/invoice_section.dart +++ b/lib/features/cart/presentation/widgets/invoice_section.dart @@ -1,35 +1,30 @@ /// Invoice Section Widget /// -/// Optional invoice information form section. +/// Optional invoice information section with address selection. 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/cart/presentation/widgets/checkout_text_field.dart'; +import 'package:worker/features/account/presentation/providers/address_provider.dart'; /// Invoice Section /// -/// Collects invoice/VAT information when checkbox is enabled. -class InvoiceSection extends HookWidget { - final ValueNotifier needsInvoice; - final TextEditingController companyNameController; - final TextEditingController taxIdController; - final TextEditingController companyAddressController; - final TextEditingController companyEmailController; +/// Shows invoice toggle and default address when enabled. +/// Matches HTML design from checkout.html. +class InvoiceSection extends HookConsumerWidget { + const InvoiceSection({super.key, required this.needsInvoice}); - const InvoiceSection({ - super.key, - required this.needsInvoice, - required this.companyNameController, - required this.taxIdController, - required this.companyAddressController, - required this.companyEmailController, - }); + final ValueNotifier needsInvoice; @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { + // Watch the default address + final defaultAddr = ref.watch(defaultAddressProvider); + return Container( margin: const EdgeInsets.symmetric(horizontal: AppSpacing.md), padding: const EdgeInsets.all(AppSpacing.md), @@ -47,19 +42,18 @@ class InvoiceSection extends HookWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Invoice Checkbox + // Header with Toggle Row( children: [ - Checkbox( - value: needsInvoice.value, - onChanged: (value) { - needsInvoice.value = value ?? false; - }, - activeColor: AppColors.primaryBlue, + const FaIcon( + FontAwesomeIcons.fileInvoice, + color: AppColors.primaryBlue, + size: 16, ), + const SizedBox(width: AppSpacing.sm), const Expanded( child: Text( - 'Xuất hóa đơn VAT', + 'Phát hành hóa đơn', style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, @@ -67,79 +61,148 @@ class InvoiceSection extends HookWidget { ), ), ), + // Toggle Switch + Switch( + value: needsInvoice.value, + onChanged: (value) { + needsInvoice.value = value; + }, + activeTrackColor: AppColors.primaryBlue, + ), ], ), - // Invoice Fields (visible when checkbox is checked) + // Invoice Information (visible when toggle is ON) if (needsInvoice.value) ...[ const SizedBox(height: AppSpacing.md), - - // Company Name - CheckoutTextField( - label: 'Tên công ty', - controller: companyNameController, - required: true, - validator: (value) { - if (needsInvoice.value && (value == null || value.isEmpty)) { - return 'Vui lòng nhập tên công ty'; - } - return null; - }, - ), - + const Divider(color: Color(0xFFE0E0E0)), const SizedBox(height: AppSpacing.md), - // Tax ID - CheckoutTextField( - label: 'Mã số thuế', - controller: taxIdController, - required: true, - keyboardType: TextInputType.number, - validator: (value) { - if (needsInvoice.value && (value == null || value.isEmpty)) { - return 'Vui lòng nhập mã số thuế'; - } - return null; - }, - ), + // Address Card + if (defaultAddr != null) + InkWell( + onTap: () { + // Navigate to addresses page for selection + context.push('/account/addresses'); + }, + borderRadius: BorderRadius.circular(AppRadius.sm), + child: Container( + padding: const EdgeInsets.all(AppSpacing.sm), + decoration: BoxDecoration( + border: Border.all(color: const Color(0xFFE0E0E0)), + borderRadius: BorderRadius.circular(AppRadius.sm), + ), + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Company/Address Title + Text( + defaultAddr.addressTitle, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: Color(0xFF212121), + ), + ), + const SizedBox(height: 4), - const SizedBox(height: AppSpacing.md), + // Tax Code (if available) + if (defaultAddr.taxCode != null && + defaultAddr.taxCode!.isNotEmpty) ...[ + Text( + 'Mã số thuế: ${defaultAddr.taxCode}', + style: const TextStyle( + fontSize: 12, + color: Color(0xFF757575), + ), + ), + const SizedBox(height: 2), + ], - // Company Address - CheckoutTextField( - label: 'Địa chỉ công ty', - controller: companyAddressController, - required: true, - maxLines: 2, - validator: (value) { - if (needsInvoice.value && (value == null || value.isEmpty)) { - return 'Vui lòng nhập địa chỉ công ty'; - } - return null; - }, - ), + // Phone + Text( + 'Số điện thoại: ${defaultAddr.phone}', + style: const TextStyle( + fontSize: 12, + color: Color(0xFF757575), + ), + ), + const SizedBox(height: 2), - const SizedBox(height: AppSpacing.md), + // Email (if available) + if (defaultAddr.email != null && + defaultAddr.email!.isNotEmpty) ...[ + Text( + 'Email: ${defaultAddr.email}', + style: const TextStyle( + fontSize: 12, + color: Color(0xFF757575), + ), + ), + const SizedBox(height: 2), + ], - // Company Email - CheckoutTextField( - label: 'Email nhận hóa đơn', - controller: companyEmailController, - required: true, - keyboardType: TextInputType.emailAddress, - validator: (value) { - if (needsInvoice.value && (value == null || value.isEmpty)) { - return 'Vui lòng nhập email'; - } - if (needsInvoice.value && - !RegExp( - r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$', - ).hasMatch(value!)) { - return 'Email không hợp lệ'; - } - return null; - }, - ), + // Address + Text( + 'Địa chỉ: ${defaultAddr.fullAddress}', + style: const TextStyle( + fontSize: 12, + color: Color(0xFF757575), + ), + ), + ], + ), + ), + const SizedBox(width: AppSpacing.sm), + const FaIcon( + FontAwesomeIcons.chevronRight, + size: 14, + color: Color(0xFF9E9E9E), + ), + ], + ), + ), + ) + else + // No default address - show button to add + InkWell( + onTap: () { + context.push('/account/addresses'); + }, + borderRadius: BorderRadius.circular(AppRadius.sm), + child: Container( + padding: const EdgeInsets.all(AppSpacing.md), + decoration: BoxDecoration( + border: Border.all( + color: AppColors.primaryBlue, + style: BorderStyle.solid, + ), + borderRadius: BorderRadius.circular(AppRadius.sm), + ), + child: const Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + FaIcon( + FontAwesomeIcons.plus, + size: 14, + color: AppColors.primaryBlue, + ), + SizedBox(width: AppSpacing.sm), + Text( + 'Thêm địa chỉ xuất hóa đơn', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: AppColors.primaryBlue, + ), + ), + ], + ), + ), + ), ], ], ), diff --git a/pubspec.yaml b/pubspec.yaml index 34280cb..1537c65 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 1.0.0+5 +version: 1.0.0+6 environment: sdk: ^3.10.0