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

@@ -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),
),
),
);
}
}