update address, cancel order
This commit is contained in:
@@ -268,3 +268,23 @@ curl --location 'https://land.dbiz.com//api/method/building_material.building_ma
|
||||
"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"
|
||||
}'
|
||||
@@ -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';
|
||||
|
||||
@@ -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,12 +75,14 @@ class AccountMenuItem extends StatelessWidget {
|
||||
AppColors.lightBlue.withValues(alpha: 0.1),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Center(
|
||||
child: FaIcon(
|
||||
icon,
|
||||
size: 18,
|
||||
color: iconColor ?? AppColors.primaryBlue,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: AppSpacing.md),
|
||||
|
||||
// Title and subtitle
|
||||
|
||||
@@ -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<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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<OrderItemDetailModel> items;
|
||||
final PaymentTermsInfoModel paymentTerms;
|
||||
final List<TimelineItemModel> timeline;
|
||||
@@ -33,10 +34,10 @@ class OrderDetailModel {
|
||||
order: OrderDetailInfoModel.fromJson(
|
||||
json['order'] as Map<String, dynamic>,
|
||||
),
|
||||
billingAddress: AddressInfoModel.fromJson(
|
||||
billingAddress: AddressModel.fromJson(
|
||||
json['billing_address'] as Map<String, dynamic>,
|
||||
),
|
||||
shippingAddress: AddressInfoModel.fromJson(
|
||||
shippingAddress: AddressModel.fromJson(
|
||||
json['shipping_address'] as Map<String, dynamic>,
|
||||
),
|
||||
items: (json['items'] as List<dynamic>)
|
||||
@@ -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<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
|
||||
class OrderItemDetailModel {
|
||||
const OrderItemDetailModel({
|
||||
|
||||
@@ -140,4 +140,30 @@ class OrderRepositoryImpl implements OrderRepository {
|
||||
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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<OrderItemDetail> items;
|
||||
final PaymentTermsInfo paymentTerms;
|
||||
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
|
||||
class OrderItemDetail extends Equatable {
|
||||
const OrderItemDetail({
|
||||
|
||||
@@ -44,4 +44,14 @@ abstract class OrderRepository {
|
||||
required String filePath,
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -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,7 +34,15 @@ class OrderDetailPage extends ConsumerWidget {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final orderDetailAsync = ref.watch(orderDetailProvider(orderId));
|
||||
|
||||
return Scaffold(
|
||||
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(
|
||||
@@ -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
|
||||
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<Address>(
|
||||
'/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<Address>(
|
||||
'/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<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),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,3 +189,51 @@ Future<OrderDetail> 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<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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user