update address, cancel order

This commit is contained in:
Phuoc Nguyen
2025-11-25 16:39:29 +07:00
parent 039dfb9fb5
commit 84669ac89c
11 changed files with 584 additions and 194 deletions

View File

@@ -267,4 +267,24 @@ curl --location 'https://land.dbiz.com//api/method/building_material.building_ma
"payments": [], "payments": [],
"invoices": [] "invoices": []
} }
} }
#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"
}'

View File

@@ -245,6 +245,16 @@ class ApiConstants {
/// Returns: { "message": {...} } /// Returns: { "message": {...} }
static const String getOrderDetail = '/building_material.building_material.api.sales_order.get_detail'; 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 user's orders (legacy endpoint - may be deprecated)
/// GET /orders?status={status}&page={page}&limit={limit} /// GET /orders?status={status}&page={page}&limit={limit}
static const String getOrders = '/orders'; static const String getOrders = '/orders';
@@ -253,10 +263,6 @@ class ApiConstants {
/// GET /orders/{orderId} /// GET /orders/{orderId}
static const String getOrderDetails = '/orders'; static const String getOrderDetails = '/orders';
/// Cancel order
/// POST /orders/{orderId}/cancel
static const String cancelOrder = '/orders';
/// Get payment transactions /// Get payment transactions
/// GET /payments?page={page}&limit={limit} /// GET /payments?page={page}&limit={limit}
static const String getPayments = '/payments'; static const String getPayments = '/payments';

View File

@@ -58,7 +58,7 @@ class AccountMenuItem extends StatelessWidget {
horizontal: AppSpacing.md, horizontal: AppSpacing.md,
vertical: AppSpacing.md, vertical: AppSpacing.md,
), ),
decoration: BoxDecoration( decoration: const BoxDecoration(
border: Border( border: Border(
bottom: BorderSide(color: AppColors.grey100, width: 1.0), bottom: BorderSide(color: AppColors.grey100, width: 1.0),
), ),
@@ -75,10 +75,12 @@ class AccountMenuItem extends StatelessWidget {
AppColors.lightBlue.withValues(alpha: 0.1), AppColors.lightBlue.withValues(alpha: 0.1),
shape: BoxShape.circle, shape: BoxShape.circle,
), ),
child: FaIcon( child: Center(
icon, child: FaIcon(
size: 18, icon,
color: iconColor ?? AppColors.primaryBlue, size: 18,
color: iconColor ?? AppColors.primaryBlue,
),
), ),
), ),
const SizedBox(width: AppSpacing.md), const SizedBox(width: AppSpacing.md),

View File

@@ -324,4 +324,48 @@ class OrderRemoteDataSource {
throw Exception('Failed to get order detail: $e'); 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<void> 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<void> cancelOrder(String orderId) async {
try {
await _dioClient.post(
'${ApiConstants.frappeApiMethod}${ApiConstants.cancelOrder}',
data: {'name': orderId},
);
} catch (e) {
throw Exception('Failed to cancel order: $e');
}
}
} }

View File

@@ -3,6 +3,7 @@
/// Data model for order detail API response. /// Data model for order detail API response.
library; library;
import 'package:worker/features/account/data/models/address_model.dart';
import 'package:worker/features/orders/domain/entities/order_detail.dart'; import 'package:worker/features/orders/domain/entities/order_detail.dart';
/// Order Detail Model /// Order Detail Model
@@ -19,8 +20,8 @@ class OrderDetailModel {
}); });
final OrderDetailInfoModel order; final OrderDetailInfoModel order;
final AddressInfoModel billingAddress; final AddressModel billingAddress;
final AddressInfoModel shippingAddress; final AddressModel shippingAddress;
final List<OrderItemDetailModel> items; final List<OrderItemDetailModel> items;
final PaymentTermsInfoModel paymentTerms; final PaymentTermsInfoModel paymentTerms;
final List<TimelineItemModel> timeline; final List<TimelineItemModel> timeline;
@@ -33,10 +34,10 @@ class OrderDetailModel {
order: OrderDetailInfoModel.fromJson( order: OrderDetailInfoModel.fromJson(
json['order'] as Map<String, dynamic>, json['order'] as Map<String, dynamic>,
), ),
billingAddress: AddressInfoModel.fromJson( billingAddress: AddressModel.fromJson(
json['billing_address'] as Map<String, dynamic>, json['billing_address'] as Map<String, dynamic>,
), ),
shippingAddress: AddressInfoModel.fromJson( shippingAddress: AddressModel.fromJson(
json['shipping_address'] as Map<String, dynamic>, json['shipping_address'] as Map<String, dynamic>,
), ),
items: (json['items'] as List<dynamic>) items: (json['items'] as List<dynamic>)
@@ -93,8 +94,8 @@ class OrderDetailModel {
factory OrderDetailModel.fromEntity(OrderDetail entity) { factory OrderDetailModel.fromEntity(OrderDetail entity) {
return OrderDetailModel( return OrderDetailModel(
order: OrderDetailInfoModel.fromEntity(entity.order), order: OrderDetailInfoModel.fromEntity(entity.order),
billingAddress: AddressInfoModel.fromEntity(entity.billingAddress), billingAddress: AddressModel.fromEntity(entity.billingAddress),
shippingAddress: AddressInfoModel.fromEntity(entity.shippingAddress), shippingAddress: AddressModel.fromEntity(entity.shippingAddress),
items: entity.items items: entity.items
.map((item) => OrderItemDetailModel.fromEntity(item)) .map((item) => OrderItemDetailModel.fromEntity(item))
.toList(), .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<String, dynamic> 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<String, dynamic> 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 /// Order Item Detail Model
class OrderItemDetailModel { class OrderItemDetailModel {
const OrderItemDetailModel({ const OrderItemDetailModel({

View File

@@ -140,4 +140,30 @@ class OrderRepositoryImpl implements OrderRepository {
throw Exception('Failed to upload bill: $e'); throw Exception('Failed to upload bill: $e');
} }
} }
@override
Future<void> 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<void> cancelOrder(String orderId) async {
try {
await _remoteDataSource.cancelOrder(orderId);
} catch (e) {
throw Exception('Failed to cancel order: $e');
}
}
} }

View File

@@ -4,6 +4,7 @@
library; library;
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:worker/features/account/domain/entities/address.dart';
/// Order Detail Entity /// Order Detail Entity
class OrderDetail extends Equatable { class OrderDetail extends Equatable {
@@ -19,8 +20,8 @@ class OrderDetail extends Equatable {
}); });
final OrderDetailInfo order; final OrderDetailInfo order;
final AddressInfo billingAddress; final Address billingAddress;
final AddressInfo shippingAddress; final Address shippingAddress;
final List<OrderItemDetail> items; final List<OrderItemDetail> items;
final PaymentTermsInfo paymentTerms; final PaymentTermsInfo paymentTerms;
final List<TimelineItem> timeline; final List<TimelineItem> 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<Object?> get props => [
name,
addressTitle,
addressLine1,
phone,
email,
fax,
taxCode,
cityCode,
wardCode,
cityName,
wardName,
isAllowEdit,
];
}
/// Order Item Detail /// Order Item Detail
class OrderItemDetail extends Equatable { class OrderItemDetail extends Equatable {
const OrderItemDetail({ const OrderItemDetail({

View File

@@ -44,4 +44,14 @@ abstract class OrderRepository {
required String filePath, required String filePath,
required String orderId, required String orderId,
}); });
/// Update order address
Future<void> updateOrderAddress({
required String orderId,
required String shippingAddressName,
required String customerAddress,
});
/// Cancel order
Future<void> cancelOrder(String orderId);
} }

View File

@@ -13,6 +13,7 @@ import 'package:worker/core/constants/ui_constants.dart';
import 'package:worker/core/enums/status_color.dart'; import 'package:worker/core/enums/status_color.dart';
import 'package:worker/core/theme/colors.dart'; import 'package:worker/core/theme/colors.dart';
import 'package:worker/core/utils/extensions.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/domain/entities/order_detail.dart';
import 'package:worker/features/orders/presentation/providers/orders_provider.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) { Widget build(BuildContext context, WidgetRef ref) {
final orderDetailAsync = ref.watch(orderDetailProvider(orderId)); final orderDetailAsync = ref.watch(orderDetailProvider(orderId));
return Scaffold( return PopScope(
backgroundColor: const Color(0xFFF4F6F8), onPopInvoked: (didPop) {
appBar: AppBar( if (didPop) {
leading: IconButton( // Dispose providers when leaving the page
icon: const FaIcon(FontAwesomeIcons.arrowLeft, color: Colors.black, size: 20), ref.invalidate(updateOrderAddressProvider);
onPressed: () => context.pop(), 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( title: const Text(
'Chi tiết đơn hàng', 'Chi tiết đơn hàng',
style: TextStyle(color: Colors.black), style: TextStyle(color: Colors.black),
@@ -83,10 +92,10 @@ class OrderDetailPage extends ConsumerWidget {
_buildStatusTimelineCard(orderDetail), _buildStatusTimelineCard(orderDetail),
// Delivery/Address Information Card // Delivery/Address Information Card
_buildAddressInfoCard(context, orderDetail), _buildAddressInfoCard(context, ref, orderDetail),
// Invoice Information Card // Invoice Information Card
_buildInvoiceInfoCard(context, orderDetail), _buildInvoiceInfoCard(context, ref, orderDetail),
// Invoices List Card // Invoices List Card
_buildInvoicesListCard(context, orderDetail), _buildInvoicesListCard(context, orderDetail),
@@ -98,7 +107,15 @@ class OrderDetailPage extends ConsumerWidget {
_buildOrderSummaryCard(orderDetail), _buildOrderSummaryCard(orderDetail),
// Payment History Card // 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), const SizedBox(height: 16),
], ],
@@ -163,6 +180,7 @@ class OrderDetailPage extends ConsumerWidget {
), ),
), ),
), ),
),
); );
} }
@@ -336,7 +354,11 @@ class OrderDetailPage extends ConsumerWidget {
} }
/// Build Address Info Card /// Build Address Info Card
Widget _buildAddressInfoCard(BuildContext context, OrderDetail orderDetail) { Widget _buildAddressInfoCard(
BuildContext context,
WidgetRef ref,
OrderDetail orderDetail,
) {
final order = orderDetail.order; final order = orderDetail.order;
final shippingAddress = orderDetail.shippingAddress; final shippingAddress = orderDetail.shippingAddress;
final dateFormatter = DateFormat('dd/MM/yyyy'); final dateFormatter = DateFormat('dd/MM/yyyy');
@@ -384,11 +406,71 @@ class OrderDetailPage extends ConsumerWidget {
), ),
), ),
TextButton( TextButton(
onPressed: () { onPressed: () async {
// TODO: Navigate to address update // Navigate to address selection and wait for result
ScaffoldMessenger.of(context).showSnackBar( final result = await context.push<Address>(
const SnackBar(content: Text('Chức năng đang phát triển')), '/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( style: TextButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
@@ -501,7 +583,11 @@ class OrderDetailPage extends ConsumerWidget {
} }
/// Build Invoice Info Card /// Build Invoice Info Card
Widget _buildInvoiceInfoCard(BuildContext context, OrderDetail orderDetail) { Widget _buildInvoiceInfoCard(
BuildContext context,
WidgetRef ref,
OrderDetail orderDetail,
) {
final billingAddress = orderDetail.billingAddress; final billingAddress = orderDetail.billingAddress;
return Card( return Card(
@@ -536,17 +622,77 @@ class OrderDetailPage extends ConsumerWidget {
], ],
), ),
TextButton( TextButton(
onPressed: () { onPressed: () async {
// TODO: Navigate to invoice update // Navigate to address selection and wait for result
ScaffoldMessenger.of(context).showSnackBar( final result = await context.push<Address>(
const SnackBar(content: Text('Chức năng đang phát triển')), '/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( style: TextButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
minimumSize: Size.zero, minimumSize: Size.zero,
tapTargetSize: MaterialTapTargetSize.shrinkWrap, tapTargetSize: MaterialTapTargetSize.shrinkWrap,
side: BorderSide(color: AppColors.grey100), side: const BorderSide(color: AppColors.grey100),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
), ),
@@ -583,7 +729,8 @@ class OrderDetailPage extends ConsumerWidget {
color: AppColors.grey900, color: AppColors.grey900,
), ),
), ),
if (billingAddress.taxCode.isNotEmpty) ...[ if (billingAddress.taxCode != null &&
billingAddress.taxCode!.isNotEmpty) ...[
const SizedBox(height: 2), const SizedBox(height: 2),
Text( Text(
'Mã số thuế: ${billingAddress.taxCode}', 'Mã số thuế: ${billingAddress.taxCode}',
@@ -949,15 +1096,15 @@ class OrderDetailPage extends ConsumerWidget {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Row( const Row(
children: [ children: [
const FaIcon( FaIcon(
FontAwesomeIcons.receipt, FontAwesomeIcons.receipt,
color: AppColors.primaryBlue, color: AppColors.primaryBlue,
size: 18, size: 18,
), ),
const SizedBox(width: 8), SizedBox(width: 8),
const Text( Text(
'Tổng kết đơn hàng', 'Tổng kết đơn hàng',
style: TextStyle( style: TextStyle(
fontSize: 16, fontSize: 16,
@@ -993,15 +1140,15 @@ class OrderDetailPage extends ConsumerWidget {
const Divider(height: 24), const Divider(height: 24),
// Payment Terms // Payment Terms
Row( const Row(
children: [ children: [
const FaIcon( FaIcon(
FontAwesomeIcons.creditCard, FontAwesomeIcons.creditCard,
size: 14, size: 14,
color: AppColors.grey500, color: AppColors.grey500,
), ),
const SizedBox(width: 6), SizedBox(width: 6),
const Text( Text(
'Điều khoản thanh toán:', 'Điều khoản thanh toán:',
style: TextStyle(fontSize: 14, color: AppColors.grey500), 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<bool>(
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<Color>(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),
),
),
);
}
} }

View File

@@ -189,3 +189,51 @@ Future<OrderDetail> orderDetail(Ref ref, String orderId) async {
final repository = await ref.watch(orderRepositoryProvider.future); final repository = await ref.watch(orderRepositoryProvider.future);
return await repository.getOrderDetail(orderId); 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<void> build() {
// No initial state needed
}
/// Update order address
Future<void> 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<void> build() {
// No initial state needed
}
/// Cancel order
Future<void> cancel(String orderId) async {
state = const AsyncValue.loading();
state = await AsyncValue.guard(() async {
final repository = await ref.read(orderRepositoryProvider.future);
await repository.cancelOrder(orderId);
});
}
}

View File

@@ -492,3 +492,122 @@ final class OrderDetailFamily extends $Family
@override @override
String toString() => r'orderDetailProvider'; 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<UpdateOrderAddress, void> {
/// 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<void> {
FutureOr<void> build();
@$mustCallSuper
@override
void runBuild() {
build();
final ref = this.ref as $Ref<AsyncValue<void>, void>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<AsyncValue<void>, void>,
AsyncValue<void>,
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<CancelOrder, void> {
/// 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<void> {
FutureOr<void> build();
@$mustCallSuper
@override
void runBuild() {
build();
final ref = this.ref as $Ref<AsyncValue<void>, void>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<AsyncValue<void>, void>,
AsyncValue<void>,
Object?,
Object?
>;
element.handleValue(ref, null);
}
}