update address, cancel order
This commit is contained in:
@@ -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<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