diff --git a/docs/order.sh b/docs/order.sh index 3437c76..54e23c5 100644 --- a/docs/order.sh +++ b/docs/order.sh @@ -267,4 +267,24 @@ curl --location 'https://land.dbiz.com//api/method/building_material.building_ma "payments": [], "invoices": [] } -} \ No newline at end of file +} + +#update address order +curl --location 'https://land.dbiz.com//api/method/building_material.building_material.api.sales_order.update' \ +--header 'Cookie: sid=a0cbe3ea6f9a7e9cf083bbe3139eada68d2357eac0167bcc66cda17d; full_name=Ha%20Duy%20Lam; sid=a0cbe3ea6f9a7e9cf083bbe3139eada68d2357eac0167bcc66cda17d; system_user=yes; user_id=lamhd%40gmail.com; user_image=/files/avatar_0986788766_1763627962.jpg' \ +--header 'X-Frappe-Csrf-Token: 6ff3be4d1f887dbebf86ba4502b05d94b30c0b0569de49b74a7171a9' \ +--header 'Content-Type: application/json' \ +--data '{ + "name" : "SAL-ORD-2025-00053", + "shipping_address_name": "Công ty Tiến Nguyễn 2-thanh toán", + "customer_address": "Nguyễn Lê Duy Ti-Billing" +}' + +#cancel order +curl --location 'https://land.dbiz.com//api/method/building_material.building_material.api.sales_order.cancel' \ +--header 'Cookie: sid=a0cbe3ea6f9a7e9cf083bbe3139eada68d2357eac0167bcc66cda17d; full_name=Ha%20Duy%20Lam; sid=a0cbe3ea6f9a7e9cf083bbe3139eada68d2357eac0167bcc66cda17d; system_user=yes; user_id=lamhd%40gmail.com; user_image=/files/avatar_0986788766_1763627962.jpg' \ +--header 'X-Frappe-Csrf-Token: 6ff3be4d1f887dbebf86ba4502b05d94b30c0b0569de49b74a7171a9' \ +--header 'Content-Type: application/json' \ +--data '{ + "name" : "SAL-ORD-2025-00054" +}' \ No newline at end of file diff --git a/lib/core/constants/api_constants.dart b/lib/core/constants/api_constants.dart index e40e1c9..55123ea 100644 --- a/lib/core/constants/api_constants.dart +++ b/lib/core/constants/api_constants.dart @@ -245,6 +245,16 @@ class ApiConstants { /// Returns: { "message": {...} } static const String getOrderDetail = '/building_material.building_material.api.sales_order.get_detail'; + /// Update order address (requires sid and csrf_token) + /// POST /api/method/building_material.building_material.api.sales_order.update + /// Body: { "name": "SAL-ORD-2025-00053", "shipping_address_name": "...", "customer_address": "..." } + static const String updateOrder = '/building_material.building_material.api.sales_order.update'; + + /// Cancel order (requires sid and csrf_token) + /// POST /api/method/building_material.building_material.api.sales_order.cancel + /// Body: { "name": "SAL-ORD-2025-00054" } + static const String cancelOrder = '/building_material.building_material.api.sales_order.cancel'; + /// Get user's orders (legacy endpoint - may be deprecated) /// GET /orders?status={status}&page={page}&limit={limit} static const String getOrders = '/orders'; @@ -253,10 +263,6 @@ class ApiConstants { /// GET /orders/{orderId} static const String getOrderDetails = '/orders'; - /// Cancel order - /// POST /orders/{orderId}/cancel - static const String cancelOrder = '/orders'; - /// Get payment transactions /// GET /payments?page={page}&limit={limit} static const String getPayments = '/payments'; diff --git a/lib/features/account/presentation/widgets/account_menu_item.dart b/lib/features/account/presentation/widgets/account_menu_item.dart index 3bba42e..87caef0 100644 --- a/lib/features/account/presentation/widgets/account_menu_item.dart +++ b/lib/features/account/presentation/widgets/account_menu_item.dart @@ -58,7 +58,7 @@ class AccountMenuItem extends StatelessWidget { horizontal: AppSpacing.md, vertical: AppSpacing.md, ), - decoration: BoxDecoration( + decoration: const BoxDecoration( border: Border( bottom: BorderSide(color: AppColors.grey100, width: 1.0), ), @@ -75,10 +75,12 @@ class AccountMenuItem extends StatelessWidget { AppColors.lightBlue.withValues(alpha: 0.1), shape: BoxShape.circle, ), - child: FaIcon( - icon, - size: 18, - color: iconColor ?? AppColors.primaryBlue, + child: Center( + child: FaIcon( + icon, + size: 18, + color: iconColor ?? AppColors.primaryBlue, + ), ), ), const SizedBox(width: AppSpacing.md), diff --git a/lib/features/orders/data/datasources/order_remote_datasource.dart b/lib/features/orders/data/datasources/order_remote_datasource.dart index 314a827..cb313bd 100644 --- a/lib/features/orders/data/datasources/order_remote_datasource.dart +++ b/lib/features/orders/data/datasources/order_remote_datasource.dart @@ -324,4 +324,48 @@ class OrderRemoteDataSource { throw Exception('Failed to get order detail: $e'); } } + + /// Update order address + /// + /// Calls: POST /api/method/building_material.building_material.api.sales_order.update + /// Body: { + /// "name": "SAL-ORD-2025-00053", + /// "shipping_address_name": "Công ty Tiến Nguyễn 2-thanh toán", + /// "customer_address": "Nguyễn Lê Duy Ti-Billing" + /// } + /// Updates shipping and billing addresses for an existing order + Future updateOrderAddress({ + required String orderId, + required String shippingAddressName, + required String customerAddress, + }) async { + try { + await _dioClient.post( + '${ApiConstants.frappeApiMethod}${ApiConstants.updateOrder}', + data: { + 'name': orderId, + 'shipping_address_name': shippingAddressName, + 'customer_address': customerAddress, + }, + ); + } catch (e) { + throw Exception('Failed to update order address: $e'); + } + } + + /// Cancel order + /// + /// Calls: POST /api/method/building_material.building_material.api.sales_order.cancel + /// Body: { "name": "SAL-ORD-2025-00054" } + /// Cancels an existing order (only allowed for "Chờ phê duyệt" status) + Future cancelOrder(String orderId) async { + try { + await _dioClient.post( + '${ApiConstants.frappeApiMethod}${ApiConstants.cancelOrder}', + data: {'name': orderId}, + ); + } catch (e) { + throw Exception('Failed to cancel order: $e'); + } + } } diff --git a/lib/features/orders/data/models/order_detail_model.dart b/lib/features/orders/data/models/order_detail_model.dart index 6fd15d6..3417b7c 100644 --- a/lib/features/orders/data/models/order_detail_model.dart +++ b/lib/features/orders/data/models/order_detail_model.dart @@ -3,6 +3,7 @@ /// Data model for order detail API response. library; +import 'package:worker/features/account/data/models/address_model.dart'; import 'package:worker/features/orders/domain/entities/order_detail.dart'; /// Order Detail Model @@ -19,8 +20,8 @@ class OrderDetailModel { }); final OrderDetailInfoModel order; - final AddressInfoModel billingAddress; - final AddressInfoModel shippingAddress; + final AddressModel billingAddress; + final AddressModel shippingAddress; final List items; final PaymentTermsInfoModel paymentTerms; final List timeline; @@ -33,10 +34,10 @@ class OrderDetailModel { order: OrderDetailInfoModel.fromJson( json['order'] as Map, ), - billingAddress: AddressInfoModel.fromJson( + billingAddress: AddressModel.fromJson( json['billing_address'] as Map, ), - shippingAddress: AddressInfoModel.fromJson( + shippingAddress: AddressModel.fromJson( json['shipping_address'] as Map, ), items: (json['items'] as List) @@ -93,8 +94,8 @@ class OrderDetailModel { factory OrderDetailModel.fromEntity(OrderDetail entity) { return OrderDetailModel( order: OrderDetailInfoModel.fromEntity(entity.order), - billingAddress: AddressInfoModel.fromEntity(entity.billingAddress), - shippingAddress: AddressInfoModel.fromEntity(entity.shippingAddress), + billingAddress: AddressModel.fromEntity(entity.billingAddress), + shippingAddress: AddressModel.fromEntity(entity.shippingAddress), items: entity.items .map((item) => OrderItemDetailModel.fromEntity(item)) .toList(), @@ -229,105 +230,6 @@ class OrderDetailInfoModel { } } -/// Address Info Model -class AddressInfoModel { - const AddressInfoModel({ - required this.name, - required this.addressTitle, - required this.addressLine1, - required this.phone, - required this.email, - this.fax, - required this.taxCode, - required this.cityCode, - required this.wardCode, - required this.cityName, - required this.wardName, - required this.isAllowEdit, - }); - - final String name; - final String addressTitle; - final String addressLine1; - final String phone; - final String email; - final String? fax; - final String taxCode; - final String cityCode; - final String wardCode; - final String cityName; - final String wardName; - final bool isAllowEdit; - - factory AddressInfoModel.fromJson(Map json) { - return AddressInfoModel( - name: json['name'] as String, - addressTitle: json['address_title'] as String, - addressLine1: json['address_line1'] as String, - phone: json['phone'] as String, - email: json['email'] as String, - fax: json['fax'] as String?, - taxCode: json['tax_code'] as String, - cityCode: json['city_code'] as String, - wardCode: json['ward_code'] as String, - cityName: json['city_name'] as String, - wardName: json['ward_name'] as String, - isAllowEdit: json['is_allow_edit'] as bool, - ); - } - - Map toJson() { - return { - 'name': name, - 'address_title': addressTitle, - 'address_line1': addressLine1, - 'phone': phone, - 'email': email, - 'fax': fax, - 'tax_code': taxCode, - 'city_code': cityCode, - 'ward_code': wardCode, - 'city_name': cityName, - 'ward_name': wardName, - 'is_allow_edit': isAllowEdit, - }; - } - - AddressInfo toEntity() { - return AddressInfo( - name: name, - addressTitle: addressTitle, - addressLine1: addressLine1, - phone: phone, - email: email, - fax: fax, - taxCode: taxCode, - cityCode: cityCode, - wardCode: wardCode, - cityName: cityName, - wardName: wardName, - isAllowEdit: isAllowEdit, - ); - } - - factory AddressInfoModel.fromEntity(AddressInfo entity) { - return AddressInfoModel( - name: entity.name, - addressTitle: entity.addressTitle, - addressLine1: entity.addressLine1, - phone: entity.phone, - email: entity.email, - fax: entity.fax, - taxCode: entity.taxCode, - cityCode: entity.cityCode, - wardCode: entity.wardCode, - cityName: entity.cityName, - wardName: entity.wardName, - isAllowEdit: entity.isAllowEdit, - ); - } -} - /// Order Item Detail Model class OrderItemDetailModel { const OrderItemDetailModel({ diff --git a/lib/features/orders/data/repositories/order_repository_impl.dart b/lib/features/orders/data/repositories/order_repository_impl.dart index 7b28dc5..69c823a 100644 --- a/lib/features/orders/data/repositories/order_repository_impl.dart +++ b/lib/features/orders/data/repositories/order_repository_impl.dart @@ -140,4 +140,30 @@ class OrderRepositoryImpl implements OrderRepository { throw Exception('Failed to upload bill: $e'); } } + + @override + Future updateOrderAddress({ + required String orderId, + required String shippingAddressName, + required String customerAddress, + }) async { + try { + await _remoteDataSource.updateOrderAddress( + orderId: orderId, + shippingAddressName: shippingAddressName, + customerAddress: customerAddress, + ); + } catch (e) { + throw Exception('Failed to update order address: $e'); + } + } + + @override + Future cancelOrder(String orderId) async { + try { + await _remoteDataSource.cancelOrder(orderId); + } catch (e) { + throw Exception('Failed to cancel order: $e'); + } + } } diff --git a/lib/features/orders/domain/entities/order_detail.dart b/lib/features/orders/domain/entities/order_detail.dart index 7dab533..c2255fa 100644 --- a/lib/features/orders/domain/entities/order_detail.dart +++ b/lib/features/orders/domain/entities/order_detail.dart @@ -4,6 +4,7 @@ library; import 'package:equatable/equatable.dart'; +import 'package:worker/features/account/domain/entities/address.dart'; /// Order Detail Entity class OrderDetail extends Equatable { @@ -19,8 +20,8 @@ class OrderDetail extends Equatable { }); final OrderDetailInfo order; - final AddressInfo billingAddress; - final AddressInfo shippingAddress; + final Address billingAddress; + final Address shippingAddress; final List items; final PaymentTermsInfo paymentTerms; final List timeline; @@ -96,53 +97,6 @@ class OrderDetailInfo extends Equatable { ]; } -/// Address Info -class AddressInfo extends Equatable { - const AddressInfo({ - required this.name, - required this.addressTitle, - required this.addressLine1, - required this.phone, - required this.email, - this.fax, - required this.taxCode, - required this.cityCode, - required this.wardCode, - required this.cityName, - required this.wardName, - required this.isAllowEdit, - }); - - final String name; - final String addressTitle; - final String addressLine1; - final String phone; - final String email; - final String? fax; - final String taxCode; - final String cityCode; - final String wardCode; - final String cityName; - final String wardName; - final bool isAllowEdit; - - @override - List get props => [ - name, - addressTitle, - addressLine1, - phone, - email, - fax, - taxCode, - cityCode, - wardCode, - cityName, - wardName, - isAllowEdit, - ]; -} - /// Order Item Detail class OrderItemDetail extends Equatable { const OrderItemDetail({ diff --git a/lib/features/orders/domain/repositories/order_repository.dart b/lib/features/orders/domain/repositories/order_repository.dart index 69a5859..1553646 100644 --- a/lib/features/orders/domain/repositories/order_repository.dart +++ b/lib/features/orders/domain/repositories/order_repository.dart @@ -44,4 +44,14 @@ abstract class OrderRepository { required String filePath, required String orderId, }); + + /// Update order address + Future updateOrderAddress({ + required String orderId, + required String shippingAddressName, + required String customerAddress, + }); + + /// Cancel order + Future cancelOrder(String orderId); } diff --git a/lib/features/orders/presentation/pages/order_detail_page.dart b/lib/features/orders/presentation/pages/order_detail_page.dart index 669073f..5cf1737 100644 --- a/lib/features/orders/presentation/pages/order_detail_page.dart +++ b/lib/features/orders/presentation/pages/order_detail_page.dart @@ -13,6 +13,7 @@ import 'package:worker/core/constants/ui_constants.dart'; import 'package:worker/core/enums/status_color.dart'; import 'package:worker/core/theme/colors.dart'; import 'package:worker/core/utils/extensions.dart'; +import 'package:worker/features/account/domain/entities/address.dart'; import 'package:worker/features/orders/domain/entities/order_detail.dart'; import 'package:worker/features/orders/presentation/providers/orders_provider.dart'; @@ -33,13 +34,21 @@ class OrderDetailPage extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final orderDetailAsync = ref.watch(orderDetailProvider(orderId)); - return Scaffold( - backgroundColor: const Color(0xFFF4F6F8), - appBar: AppBar( - leading: IconButton( - icon: const FaIcon(FontAwesomeIcons.arrowLeft, color: Colors.black, size: 20), - onPressed: () => context.pop(), - ), + return PopScope( + onPopInvoked: (didPop) { + if (didPop) { + // Dispose providers when leaving the page + ref.invalidate(updateOrderAddressProvider); + ref.invalidate(cancelOrderProvider); + } + }, + child: Scaffold( + backgroundColor: const Color(0xFFF4F6F8), + appBar: AppBar( + leading: IconButton( + icon: const FaIcon(FontAwesomeIcons.arrowLeft, color: Colors.black, size: 20), + onPressed: () => context.pop(), + ), title: const Text( 'Chi tiết đơn hàng', style: TextStyle(color: Colors.black), @@ -83,10 +92,10 @@ class OrderDetailPage extends ConsumerWidget { _buildStatusTimelineCard(orderDetail), // Delivery/Address Information Card - _buildAddressInfoCard(context, orderDetail), + _buildAddressInfoCard(context, ref, orderDetail), // Invoice Information Card - _buildInvoiceInfoCard(context, orderDetail), + _buildInvoiceInfoCard(context, ref, orderDetail), // Invoices List Card _buildInvoicesListCard(context, orderDetail), @@ -98,7 +107,15 @@ class OrderDetailPage extends ConsumerWidget { _buildOrderSummaryCard(orderDetail), // Payment History Card - _buildPaymentHistoryCard(context, orderDetail), + if (orderDetail.order.totalRemaining > 0) ...[ + _buildPaymentHistoryCard(context, orderDetail), + ], + + // Cancel Order Button (only show for "Chờ phê duyệt" status) + if (orderDetail.order.status == 'Chờ phê duyệt') ...[ + const SizedBox(height: 16), + _buildCancelOrderButton(context, ref, orderDetail), + ], const SizedBox(height: 16), ], @@ -163,6 +180,7 @@ class OrderDetailPage extends ConsumerWidget { ), ), ), + ), ); } @@ -336,7 +354,11 @@ class OrderDetailPage extends ConsumerWidget { } /// Build Address Info Card - Widget _buildAddressInfoCard(BuildContext context, OrderDetail orderDetail) { + Widget _buildAddressInfoCard( + BuildContext context, + WidgetRef ref, + OrderDetail orderDetail, + ) { final order = orderDetail.order; final shippingAddress = orderDetail.shippingAddress; final dateFormatter = DateFormat('dd/MM/yyyy'); @@ -384,11 +406,71 @@ class OrderDetailPage extends ConsumerWidget { ), ), TextButton( - onPressed: () { - // TODO: Navigate to address update - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Chức năng đang phát triển')), + onPressed: () async { + // Navigate to address selection and wait for result + final result = await context.push
( + '/account/addresses', + extra: { + 'selectMode': true, + 'currentAddress': shippingAddress, + }, ); + + // If user selected an address, update the order + if (result != null && context.mounted) { + // Show loading indicator + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Đang cập nhật địa chỉ...'), + duration: Duration(seconds: 1), + ), + ); + + // Update shipping address (keep billing address the same) + await ref + .read(updateOrderAddressProvider.notifier) + .updateAddress( + orderId: orderId, + shippingAddressName: result.name, + customerAddress: orderDetail.billingAddress.name, + ); + + // Check if update was successful + final updateState = + ref.read(updateOrderAddressProvider) + ..when( + data: (_) { + // Refresh order detail to show updated address + ref.invalidate(orderDetailProvider(orderId)); + + // Show success message + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text( + 'Cập nhật địa chỉ giao hàng thành công', + ), + backgroundColor: AppColors.success, + ), + ); + } + }, + error: (error, _) { + // Show error message + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + 'Lỗi: ${error.toString()}', + ), + backgroundColor: AppColors.danger, + ), + ); + } + }, + loading: () {}, + ); + } }, style: TextButton.styleFrom( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), @@ -501,7 +583,11 @@ class OrderDetailPage extends ConsumerWidget { } /// Build Invoice Info Card - Widget _buildInvoiceInfoCard(BuildContext context, OrderDetail orderDetail) { + Widget _buildInvoiceInfoCard( + BuildContext context, + WidgetRef ref, + OrderDetail orderDetail, + ) { final billingAddress = orderDetail.billingAddress; return Card( @@ -536,17 +622,77 @@ class OrderDetailPage extends ConsumerWidget { ], ), TextButton( - onPressed: () { - // TODO: Navigate to invoice update - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Chức năng đang phát triển')), + onPressed: () async { + // Navigate to address selection and wait for result + final result = await context.push
( + '/account/addresses', + extra: { + 'selectMode': true, + 'currentAddress': billingAddress, + }, ); + + // If user selected an address, update the order + if (result != null && context.mounted) { + // Show loading indicator + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Đang cập nhật địa chỉ...'), + duration: Duration(seconds: 1), + ), + ); + + // Update billing address (keep shipping address the same) + await ref + .read(updateOrderAddressProvider.notifier) + .updateAddress( + orderId: orderId, + shippingAddressName: orderDetail.shippingAddress.name, + customerAddress: result.name, + ); + + // Check if update was successful + final updateState = + ref.read(updateOrderAddressProvider) + ..when( + data: (_) { + // Refresh order detail to show updated address + ref.invalidate(orderDetailProvider(orderId)); + + // Show success message + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text( + 'Cập nhật địa chỉ hóa đơn thành công', + ), + backgroundColor: AppColors.success, + ), + ); + } + }, + error: (error, _) { + // Show error message + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + 'Lỗi: ${error.toString()}', + ), + backgroundColor: AppColors.danger, + ), + ); + } + }, + loading: () {}, + ); + } }, style: TextButton.styleFrom( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), minimumSize: Size.zero, tapTargetSize: MaterialTapTargetSize.shrinkWrap, - side: BorderSide(color: AppColors.grey100), + side: const BorderSide(color: AppColors.grey100), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), @@ -583,7 +729,8 @@ class OrderDetailPage extends ConsumerWidget { color: AppColors.grey900, ), ), - if (billingAddress.taxCode.isNotEmpty) ...[ + if (billingAddress.taxCode != null && + billingAddress.taxCode!.isNotEmpty) ...[ const SizedBox(height: 2), Text( 'Mã số thuế: ${billingAddress.taxCode}', @@ -949,15 +1096,15 @@ class OrderDetailPage extends ConsumerWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row( + const Row( children: [ - const FaIcon( + FaIcon( FontAwesomeIcons.receipt, color: AppColors.primaryBlue, size: 18, ), - const SizedBox(width: 8), - const Text( + SizedBox(width: 8), + Text( 'Tổng kết đơn hàng', style: TextStyle( fontSize: 16, @@ -993,15 +1140,15 @@ class OrderDetailPage extends ConsumerWidget { const Divider(height: 24), // Payment Terms - Row( + const Row( children: [ - const FaIcon( + FaIcon( FontAwesomeIcons.creditCard, size: 14, color: AppColors.grey500, ), - const SizedBox(width: 6), - const Text( + SizedBox(width: 6), + Text( 'Điều khoản thanh toán:', style: TextStyle(fontSize: 14, color: AppColors.grey500), ), @@ -1295,4 +1442,116 @@ class OrderDetailPage extends ConsumerWidget { ), ); } + + /// Build Cancel Order Button + Widget _buildCancelOrderButton( + BuildContext context, + WidgetRef ref, + OrderDetail orderDetail, + ) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: OutlinedButton.icon( + onPressed: () async { + // Show confirmation dialog + final confirmed = await showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Xác nhận hủy đơn'), + content: Text( + 'Bạn có chắc chắn muốn hủy đơn hàng ${orderDetail.order.name}?\n\nHành động này không thể hoàn tác.', + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(false), + child: const Text('Không'), + ), + ElevatedButton( + onPressed: () => Navigator.of(context).pop(true), + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.danger, + foregroundColor: Colors.white, + ), + child: const Text('Hủy đơn'), + ), + ], + ), + ); + + // If user confirmed, proceed with cancellation + if (confirmed == true && context.mounted) { + // Show loading indicator + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Row( + children: [ + SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation(Colors.white), + ), + ), + SizedBox(width: 12), + Text('Đang hủy đơn hàng...'), + ], + ), + duration: Duration(seconds: 2), + ), + ); + + // Call cancel order API + await ref.read(cancelOrderProvider.notifier).cancel(orderId); + + // Check result + final cancelState = ref.read(cancelOrderProvider); + if (context.mounted) { + cancelState.when( + data: (_) { + // Success: Invalidate order providers and show success message + ref.invalidate(ordersProvider); + ref.invalidate(orderDetailProvider(orderId)); + + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Đã hủy đơn hàng thành công'), + backgroundColor: AppColors.success, + ), + ); + + // Pop back to orders list + context.pop(); + }, + error: (error, _) { + // Error: Show error message + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Lỗi: ${error.toString()}'), + backgroundColor: AppColors.danger, + ), + ); + }, + loading: () {}, + ); + } + } + }, + icon: const FaIcon(FontAwesomeIcons.xmark, size: 18), + label: const Text('Hủy đơn hàng'), + style: OutlinedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 14), + side: const BorderSide( + color: AppColors.danger, + width: 2, + ), + foregroundColor: AppColors.danger, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + minimumSize: const Size(double.infinity, 48), + ), + ), + ); + } } diff --git a/lib/features/orders/presentation/providers/orders_provider.dart b/lib/features/orders/presentation/providers/orders_provider.dart index 3d1fcf3..88b50e7 100644 --- a/lib/features/orders/presentation/providers/orders_provider.dart +++ b/lib/features/orders/presentation/providers/orders_provider.dart @@ -189,3 +189,51 @@ Future orderDetail(Ref ref, String orderId) async { final repository = await ref.watch(orderRepositoryProvider.future); return await repository.getOrderDetail(orderId); } + +/// Update Order Address Action +/// +/// Updates the shipping and billing addresses for an order. +@Riverpod(keepAlive: true) +class UpdateOrderAddress extends _$UpdateOrderAddress { + @override + FutureOr build() { + // No initial state needed + } + + /// Update order address + Future updateAddress({ + required String orderId, + required String shippingAddressName, + required String customerAddress, + }) async { + state = const AsyncValue.loading(); + state = await AsyncValue.guard(() async { + final repository = await ref.read(orderRepositoryProvider.future); + await repository.updateOrderAddress( + orderId: orderId, + shippingAddressName: shippingAddressName, + customerAddress: customerAddress, + ); + }); + } +} + +/// Cancel Order Action +/// +/// Cancels an order (only allowed for "Chờ phê duyệt" status). +@Riverpod(keepAlive: true) +class CancelOrder extends _$CancelOrder { + @override + FutureOr build() { + // No initial state needed + } + + /// Cancel order + Future cancel(String orderId) async { + state = const AsyncValue.loading(); + state = await AsyncValue.guard(() async { + final repository = await ref.read(orderRepositoryProvider.future); + await repository.cancelOrder(orderId); + }); + } +} diff --git a/lib/features/orders/presentation/providers/orders_provider.g.dart b/lib/features/orders/presentation/providers/orders_provider.g.dart index e615919..4cd218a 100644 --- a/lib/features/orders/presentation/providers/orders_provider.g.dart +++ b/lib/features/orders/presentation/providers/orders_provider.g.dart @@ -492,3 +492,122 @@ final class OrderDetailFamily extends $Family @override String toString() => r'orderDetailProvider'; } + +/// Update Order Address Action +/// +/// Updates the shipping and billing addresses for an order. + +@ProviderFor(UpdateOrderAddress) +const updateOrderAddressProvider = UpdateOrderAddressProvider._(); + +/// Update Order Address Action +/// +/// Updates the shipping and billing addresses for an order. +final class UpdateOrderAddressProvider + extends $AsyncNotifierProvider { + /// Update Order Address Action + /// + /// Updates the shipping and billing addresses for an order. + const UpdateOrderAddressProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'updateOrderAddressProvider', + isAutoDispose: false, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$updateOrderAddressHash(); + + @$internal + @override + UpdateOrderAddress create() => UpdateOrderAddress(); +} + +String _$updateOrderAddressHash() => + r'1913c2ccc2ba232debb4368f350f64c3d08cccec'; + +/// Update Order Address Action +/// +/// Updates the shipping and billing addresses for an order. + +abstract class _$UpdateOrderAddress extends $AsyncNotifier { + FutureOr build(); + @$mustCallSuper + @override + void runBuild() { + build(); + final ref = this.ref as $Ref, void>; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier, void>, + AsyncValue, + Object?, + Object? + >; + element.handleValue(ref, null); + } +} + +/// Cancel Order Action +/// +/// Cancels an order (only allowed for "Chờ phê duyệt" status). + +@ProviderFor(CancelOrder) +const cancelOrderProvider = CancelOrderProvider._(); + +/// Cancel Order Action +/// +/// Cancels an order (only allowed for "Chờ phê duyệt" status). +final class CancelOrderProvider + extends $AsyncNotifierProvider { + /// Cancel Order Action + /// + /// Cancels an order (only allowed for "Chờ phê duyệt" status). + const CancelOrderProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'cancelOrderProvider', + isAutoDispose: false, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$cancelOrderHash(); + + @$internal + @override + CancelOrder create() => CancelOrder(); +} + +String _$cancelOrderHash() => r'201624156f3ae3c05fe438ea7e266cb8fa2a5bd6'; + +/// Cancel Order Action +/// +/// Cancels an order (only allowed for "Chờ phê duyệt" status). + +abstract class _$CancelOrder extends $AsyncNotifier { + FutureOr build(); + @$mustCallSuper + @override + void runBuild() { + build(); + final ref = this.ref as $Ref, void>; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier, void>, + AsyncValue, + Object?, + Object? + >; + element.handleValue(ref, null); + } +}