update order detail
This commit is contained in:
@@ -3,14 +3,17 @@
|
||||
/// Displays detailed order information including status timeline, delivery info, and products.
|
||||
library;
|
||||
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:worker/core/constants/ui_constants.dart';
|
||||
import 'package:worker/core/database/models/enums.dart';
|
||||
import 'package:worker/core/enums/status_color.dart';
|
||||
import 'package:worker/core/theme/colors.dart';
|
||||
import 'package:worker/features/orders/domain/entities/order_detail.dart';
|
||||
import 'package:worker/features/orders/presentation/providers/orders_provider.dart';
|
||||
|
||||
/// Order Detail Page
|
||||
///
|
||||
@@ -27,9 +30,7 @@ class OrderDetailPage extends ConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
// TODO: Replace with actual order data from provider
|
||||
// For now using mock data based on HTML reference
|
||||
final mockOrder = _getMockOrder();
|
||||
final orderDetailAsync = ref.watch(orderDetailProvider(orderId));
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFF4F6F8),
|
||||
@@ -69,136 +70,98 @@ class OrderDetailPage extends ConsumerWidget {
|
||||
foregroundColor: AppColors.grey900,
|
||||
centerTitle: false,
|
||||
),
|
||||
body: Stack(
|
||||
children: [
|
||||
SingleChildScrollView(
|
||||
padding: const EdgeInsets.only(bottom: 100),
|
||||
child: Column(
|
||||
children: [
|
||||
// Status Timeline Card
|
||||
_buildStatusTimelineCard(
|
||||
mockOrder['orderNumber']! as String,
|
||||
mockOrder['status']! as OrderStatus,
|
||||
mockOrder['statusHistory']! as List<Map<String, dynamic>>,
|
||||
body: orderDetailAsync.when(
|
||||
data: (orderDetail) {
|
||||
return Stack(
|
||||
children: [
|
||||
SingleChildScrollView(
|
||||
padding: const EdgeInsets.only(bottom: 100),
|
||||
child: Column(
|
||||
children: [
|
||||
// Status Timeline Card
|
||||
_buildStatusTimelineCard(orderDetail),
|
||||
|
||||
// Delivery/Address Information Card
|
||||
_buildAddressInfoCard(orderDetail),
|
||||
|
||||
// Customer Information Card
|
||||
_buildCustomerInfoCard(orderDetail),
|
||||
|
||||
// Products List Card
|
||||
_buildProductsListCard(orderDetail),
|
||||
|
||||
// Order Summary Card
|
||||
_buildOrderSummaryCard(orderDetail),
|
||||
],
|
||||
),
|
||||
|
||||
// Delivery Information Card
|
||||
_buildDeliveryInfoCard(
|
||||
mockOrder['deliveryMethod']! as String,
|
||||
mockOrder['warehouseDate']! as DateTime,
|
||||
mockOrder['deliveryDate']! as DateTime,
|
||||
mockOrder['deliveryAddress']! as String,
|
||||
mockOrder['receiverName']! as String,
|
||||
mockOrder['receiverPhone']! as String,
|
||||
),
|
||||
|
||||
// Customer Information Card
|
||||
_buildCustomerInfoCard(
|
||||
mockOrder['customerName']! as String,
|
||||
mockOrder['customerPhone']! as String,
|
||||
mockOrder['customerEmail']! as String,
|
||||
mockOrder['customerType']! as String,
|
||||
),
|
||||
|
||||
// Products List Card
|
||||
_buildProductsListCard(),
|
||||
|
||||
// Order Summary Card
|
||||
_buildOrderSummaryCard(
|
||||
mockOrder['subtotal']! as double,
|
||||
mockOrder['shippingFee']! as double,
|
||||
mockOrder['discount']! as double,
|
||||
mockOrder['total']! as double,
|
||||
mockOrder['paymentMethod']! as String,
|
||||
mockOrder['notes'] as String?,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Fixed Action Buttons
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.white,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.1),
|
||||
blurRadius: 15,
|
||||
offset: const Offset(0, -4),
|
||||
),
|
||||
],
|
||||
),
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
spacing: 12,
|
||||
children: [
|
||||
Expanded(
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: () {
|
||||
// TODO: Implement contact customer
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Gọi điện cho khách hàng...'),
|
||||
),
|
||||
);
|
||||
},
|
||||
icon: const FaIcon(FontAwesomeIcons.phone, size: 18),
|
||||
label: const Text('Liên hệ khách hàng'),
|
||||
style: OutlinedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
side: const BorderSide(
|
||||
color: AppColors.grey100,
|
||||
width: 2,
|
||||
),
|
||||
foregroundColor: AppColors.grey900,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
// TODO: Implement update status
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Cập nhật trạng thái...'),
|
||||
),
|
||||
);
|
||||
},
|
||||
icon: const FaIcon(FontAwesomeIcons.penToSquare, size: 18),
|
||||
label: const Text('Cập nhật trạng thái'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
backgroundColor: AppColors.primaryBlue,
|
||||
foregroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
// Fixed Action Buttons
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: _buildActionButtons(context, orderDetail),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error: (error, stack) => Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const FaIcon(
|
||||
FontAwesomeIcons.circleExclamation,
|
||||
size: 64,
|
||||
color: AppColors.danger,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
'Không thể tải thông tin đơn hàng',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.grey900,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
error.toString(),
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: AppColors.grey500,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
ref.invalidate(orderDetailProvider(orderId));
|
||||
},
|
||||
icon: const FaIcon(FontAwesomeIcons.arrowsRotate, size: 16),
|
||||
label: const Text('Thử lại'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.primaryBlue,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 24,
|
||||
vertical: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Build Status Timeline Card
|
||||
Widget _buildStatusTimelineCard(
|
||||
String orderNumber,
|
||||
OrderStatus currentStatus,
|
||||
List<Map<String, dynamic>> statusHistory,
|
||||
) {
|
||||
Widget _buildStatusTimelineCard(OrderDetail orderDetail) {
|
||||
final order = orderDetail.order;
|
||||
final timeline = orderDetail.timeline;
|
||||
|
||||
return Card(
|
||||
margin: const EdgeInsets.all(16),
|
||||
elevation: 1,
|
||||
@@ -209,33 +172,30 @@ class OrderDetailPage extends ConsumerWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Order Number and Status Badge
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'#$orderNumber',
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColors.primaryBlue,
|
||||
),
|
||||
),
|
||||
_buildStatusBadge(currentStatus),
|
||||
],
|
||||
Text(
|
||||
'#${order.name}',
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColors.primaryBlue,
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 12,),
|
||||
_buildStatusBadge(order.status, order.statusColor),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Status Timeline
|
||||
...statusHistory.asMap().entries.map((entry) {
|
||||
...timeline.asMap().entries.map((entry) {
|
||||
final index = entry.key;
|
||||
final item = entry.value;
|
||||
final isLast = index == statusHistory.length - 1;
|
||||
final isLast = index == timeline.length - 1;
|
||||
|
||||
return _buildTimelineItem(
|
||||
title: item['title']! as String,
|
||||
date: item['date'] as String?,
|
||||
status: item['status']! as String,
|
||||
title: item.label,
|
||||
date: item.value,
|
||||
status: item.status,
|
||||
isLast: isLast,
|
||||
);
|
||||
}),
|
||||
@@ -249,28 +209,28 @@ class OrderDetailPage extends ConsumerWidget {
|
||||
Widget _buildTimelineItem({
|
||||
required String title,
|
||||
String? date,
|
||||
required String status, // 'completed', 'active', 'pending'
|
||||
required String status, // 'Success', 'Warning', 'Secondary', etc.
|
||||
required bool isLast,
|
||||
}) {
|
||||
final statusColor = StatusColor.fromString(status) ?? StatusColor.secondary;
|
||||
|
||||
Color iconColor;
|
||||
Color iconBgColor;
|
||||
IconData iconData;
|
||||
|
||||
switch (status) {
|
||||
case 'completed':
|
||||
iconColor = Colors.white;
|
||||
iconBgColor = AppColors.success;
|
||||
iconData = FontAwesomeIcons.check;
|
||||
break;
|
||||
case 'active':
|
||||
iconColor = Colors.white;
|
||||
iconBgColor = AppColors.warning;
|
||||
iconData = FontAwesomeIcons.gear;
|
||||
break;
|
||||
default: // pending
|
||||
iconColor = AppColors.grey500;
|
||||
iconBgColor = AppColors.grey100;
|
||||
iconData = _getIconForTitle(title);
|
||||
if (statusColor == StatusColor.success) {
|
||||
iconColor = Colors.white;
|
||||
iconBgColor = statusColor.color;
|
||||
iconData = FontAwesomeIcons.check;
|
||||
} else if (statusColor == StatusColor.warning) {
|
||||
iconColor = Colors.white;
|
||||
iconBgColor = statusColor.color;
|
||||
iconData = FontAwesomeIcons.gear;
|
||||
} else {
|
||||
// Secondary or other
|
||||
iconColor = AppColors.grey500;
|
||||
iconBgColor = AppColors.grey100;
|
||||
iconData = _getIconForTitle(title);
|
||||
}
|
||||
|
||||
return Row(
|
||||
@@ -286,7 +246,7 @@ class OrderDetailPage extends ConsumerWidget {
|
||||
color: iconBgColor,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: FaIcon(iconData, size: 10, color: iconColor),
|
||||
child: Center(child: FaIcon(iconData, size: 10, color: iconColor)),
|
||||
),
|
||||
if (!isLast)
|
||||
Container(
|
||||
@@ -342,85 +302,34 @@ class OrderDetailPage extends ConsumerWidget {
|
||||
}
|
||||
|
||||
/// Build Status Badge
|
||||
Widget _buildStatusBadge(OrderStatus status) {
|
||||
Widget _buildStatusBadge(String status, String statusColorName) {
|
||||
final statusColor = StatusColor.fromString(statusColorName) ?? StatusColor.secondary;
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: _getStatusColor(status).withValues(alpha: 0.1),
|
||||
color: statusColor.light,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(
|
||||
color: _getStatusColor(status).withValues(alpha: 0.3),
|
||||
color: statusColor.border,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
_getStatusText(status),
|
||||
status,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: _getStatusColor(status),
|
||||
color: statusColor.color,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Get status color
|
||||
Color _getStatusColor(OrderStatus status) {
|
||||
switch (status) {
|
||||
case OrderStatus.draft:
|
||||
return AppColors.grey500;
|
||||
case OrderStatus.pending:
|
||||
return AppColors.info;
|
||||
case OrderStatus.confirmed:
|
||||
return AppColors.info;
|
||||
case OrderStatus.processing:
|
||||
return AppColors.warning;
|
||||
case OrderStatus.shipped:
|
||||
return AppColors.primaryBlue;
|
||||
case OrderStatus.delivered:
|
||||
return AppColors.success;
|
||||
case OrderStatus.completed:
|
||||
return AppColors.success;
|
||||
case OrderStatus.cancelled:
|
||||
return AppColors.danger;
|
||||
case OrderStatus.refunded:
|
||||
return const Color(0xFFF97316);
|
||||
}
|
||||
}
|
||||
|
||||
/// Get status text
|
||||
String _getStatusText(OrderStatus status) {
|
||||
switch (status) {
|
||||
case OrderStatus.draft:
|
||||
return 'Nháp';
|
||||
case OrderStatus.pending:
|
||||
return 'Chờ xác nhận';
|
||||
case OrderStatus.confirmed:
|
||||
return 'Đã xác nhận';
|
||||
case OrderStatus.processing:
|
||||
return 'Đang xử lý';
|
||||
case OrderStatus.shipped:
|
||||
return 'Đang vận chuyển';
|
||||
case OrderStatus.delivered:
|
||||
return 'Đã giao hàng';
|
||||
case OrderStatus.completed:
|
||||
return 'Hoàn thành';
|
||||
case OrderStatus.cancelled:
|
||||
return 'Đã hủy';
|
||||
case OrderStatus.refunded:
|
||||
return 'Đã hoàn tiền';
|
||||
}
|
||||
}
|
||||
|
||||
/// Build Delivery Info Card
|
||||
Widget _buildDeliveryInfoCard(
|
||||
String deliveryMethod,
|
||||
DateTime warehouseDate,
|
||||
DateTime deliveryDate,
|
||||
String deliveryAddress,
|
||||
String receiverName,
|
||||
String receiverPhone,
|
||||
) {
|
||||
/// Build Address Info Card
|
||||
Widget _buildAddressInfoCard(OrderDetail orderDetail) {
|
||||
final order = orderDetail.order;
|
||||
final shippingAddress = orderDetail.shippingAddress;
|
||||
final dateFormatter = DateFormat('dd/MM/yyyy');
|
||||
|
||||
return Card(
|
||||
@@ -453,72 +362,11 @@ class OrderDetailPage extends ConsumerWidget {
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Delivery Method
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.grey50,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.primaryBlue,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const FaIcon(
|
||||
FontAwesomeIcons.truck,
|
||||
color: Colors.white,
|
||||
size: 18,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
deliveryMethod,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.grey900,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
const Text(
|
||||
'Giao trong 3-5 ngày làm việc',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppColors.grey500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Delivery Details
|
||||
// Delivery Date
|
||||
_buildInfoRow(
|
||||
icon: FontAwesomeIcons.calendar,
|
||||
label: 'Ngày xuất kho',
|
||||
value: dateFormatter.format(warehouseDate),
|
||||
valueColor: AppColors.success,
|
||||
),
|
||||
|
||||
const SizedBox(height: 12),
|
||||
|
||||
_buildInfoRow(
|
||||
icon: FontAwesomeIcons.clock,
|
||||
label: 'Thời gian giao hàng',
|
||||
value: '${dateFormatter.format(deliveryDate)}, 8:00 - 17:00',
|
||||
label: 'Ngày giao hàng',
|
||||
value: dateFormatter.format(DateTime.parse(order.deliveryDate)),
|
||||
),
|
||||
|
||||
const SizedBox(height: 12),
|
||||
@@ -526,7 +374,8 @@ class OrderDetailPage extends ConsumerWidget {
|
||||
_buildInfoRow(
|
||||
icon: FontAwesomeIcons.locationDot,
|
||||
label: 'Địa chỉ giao hàng',
|
||||
value: deliveryAddress,
|
||||
value:
|
||||
'${shippingAddress.addressLine1}\n${shippingAddress.wardName}, ${shippingAddress.cityName}',
|
||||
),
|
||||
|
||||
const SizedBox(height: 12),
|
||||
@@ -534,7 +383,7 @@ class OrderDetailPage extends ConsumerWidget {
|
||||
_buildInfoRow(
|
||||
icon: FontAwesomeIcons.user,
|
||||
label: 'Người nhận',
|
||||
value: '$receiverName - $receiverPhone',
|
||||
value: '${shippingAddress.addressTitle} - ${shippingAddress.phone}',
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -590,12 +439,9 @@ class OrderDetailPage extends ConsumerWidget {
|
||||
}
|
||||
|
||||
/// Build Customer Info Card
|
||||
Widget _buildCustomerInfoCard(
|
||||
String customerName,
|
||||
String customerPhone,
|
||||
String customerEmail,
|
||||
String customerType,
|
||||
) {
|
||||
Widget _buildCustomerInfoCard(OrderDetail orderDetail) {
|
||||
final order = orderDetail.order;
|
||||
final billingAddress = orderDetail.billingAddress;
|
||||
return Card(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
elevation: 1,
|
||||
@@ -626,46 +472,19 @@ class OrderDetailPage extends ConsumerWidget {
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
_buildCustomerRow('Tên khách hàng:', customerName),
|
||||
_buildCustomerRow('Tên khách hàng:', order.customer),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
_buildCustomerRow('Số điện thoại:', customerPhone),
|
||||
_buildCustomerRow('Số điện thoại:', billingAddress.phone),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
_buildCustomerRow('Email:', customerEmail),
|
||||
_buildCustomerRow('Email:', billingAddress.email),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text(
|
||||
'Loại khách hàng:',
|
||||
style: TextStyle(fontSize: 14, color: AppColors.grey500),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
colors: [Color(0xFFFFD700), Color(0xFFFFA500)],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Text(
|
||||
customerType,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (billingAddress.taxCode.isNotEmpty) ...[
|
||||
_buildCustomerRow('Mã số thuế:', billingAddress.taxCode),
|
||||
const SizedBox(height: 12),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -694,25 +513,13 @@ class OrderDetailPage extends ConsumerWidget {
|
||||
}
|
||||
|
||||
/// Build Products List Card
|
||||
Widget _buildProductsListCard() {
|
||||
final products = [
|
||||
{
|
||||
'name': 'Gạch Eurotile MỘC LAM E03',
|
||||
'size': '60x60cm',
|
||||
'sku': 'ET-ML-E03-60x60',
|
||||
'quantity': '30 m²',
|
||||
'unitPrice': '285.000đ/m²',
|
||||
'totalPrice': '8.550.000đ',
|
||||
},
|
||||
{
|
||||
'name': 'Gạch Eurotile STONE GREY S02',
|
||||
'size': '80x80cm',
|
||||
'sku': 'ET-SG-S02-80x80',
|
||||
'quantity': '20 m²',
|
||||
'unitPrice': '217.500đ/m²',
|
||||
'totalPrice': '4.350.000đ',
|
||||
},
|
||||
];
|
||||
Widget _buildProductsListCard(OrderDetail orderDetail) {
|
||||
final items = orderDetail.items;
|
||||
final currencyFormatter = NumberFormat.currency(
|
||||
locale: 'vi_VN',
|
||||
symbol: 'đ',
|
||||
decimalDigits: 0,
|
||||
);
|
||||
|
||||
return Card(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
@@ -744,8 +551,8 @@ class OrderDetailPage extends ConsumerWidget {
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
...products.map(
|
||||
(product) => Container(
|
||||
...items.map(
|
||||
(item) => Container(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
@@ -756,19 +563,48 @@ class OrderDetailPage extends ConsumerWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Product Image
|
||||
Container(
|
||||
width: 60,
|
||||
height: 60,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.grey50,
|
||||
if (item.thumbnail != null)
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: item.thumbnail!,
|
||||
width: 60,
|
||||
height: 60,
|
||||
fit: BoxFit.cover,
|
||||
placeholder: (context, url) => Container(
|
||||
width: 60,
|
||||
height: 60,
|
||||
color: AppColors.grey50,
|
||||
child: const Center(
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
),
|
||||
),
|
||||
errorWidget: (context, url, error) => Container(
|
||||
width: 60,
|
||||
height: 60,
|
||||
color: AppColors.grey50,
|
||||
child: const FaIcon(
|
||||
FontAwesomeIcons.image,
|
||||
color: AppColors.grey500,
|
||||
size: 28,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
Container(
|
||||
width: 60,
|
||||
height: 60,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.grey50,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: const FaIcon(
|
||||
FontAwesomeIcons.image,
|
||||
color: AppColors.grey500,
|
||||
size: 28,
|
||||
),
|
||||
),
|
||||
child: const FaIcon(
|
||||
FontAwesomeIcons.image,
|
||||
color: AppColors.grey500,
|
||||
size: 28,
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(width: 12),
|
||||
|
||||
@@ -778,7 +614,7 @@ class OrderDetailPage extends ConsumerWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
product['name']!,
|
||||
item.itemName,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
@@ -787,14 +623,7 @@ class OrderDetailPage extends ConsumerWidget {
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Kích thước: ${product['size']}',
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppColors.grey500,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'SKU: ${product['sku']}',
|
||||
'Mã: ${item.itemCode}',
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppColors.grey500,
|
||||
@@ -815,7 +644,7 @@ class OrderDetailPage extends ConsumerWidget {
|
||||
),
|
||||
),
|
||||
Text(
|
||||
product['quantity']!,
|
||||
'${item.qtyOfSm} m²',
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
@@ -828,14 +657,14 @@ class OrderDetailPage extends ConsumerWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
product['unitPrice']!,
|
||||
'${currencyFormatter.format(item.price)}/m²',
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppColors.grey500,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
product['totalPrice']!,
|
||||
currencyFormatter.format(item.totalAmount),
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
@@ -860,14 +689,9 @@ class OrderDetailPage extends ConsumerWidget {
|
||||
}
|
||||
|
||||
/// Build Order Summary Card
|
||||
Widget _buildOrderSummaryCard(
|
||||
double subtotal,
|
||||
double shippingFee,
|
||||
double discount,
|
||||
double total,
|
||||
String paymentMethod,
|
||||
String? notes,
|
||||
) {
|
||||
Widget _buildOrderSummaryCard(OrderDetail orderDetail) {
|
||||
final order = orderDetail.order;
|
||||
final paymentTerms = orderDetail.paymentTerms;
|
||||
final currencyFormatter = NumberFormat.currency(
|
||||
locale: 'vi_VN',
|
||||
symbol: 'đ',
|
||||
@@ -904,35 +728,29 @@ class OrderDetailPage extends ConsumerWidget {
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
_buildSummaryRow('Tạm tính:', currencyFormatter.format(subtotal)),
|
||||
_buildSummaryRow('Tổng tiền hàng:', currencyFormatter.format(order.total)),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
_buildSummaryRow(
|
||||
'Phí vận chuyển:',
|
||||
shippingFee == 0
|
||||
? 'Miễn phí'
|
||||
: currencyFormatter.format(shippingFee),
|
||||
valueColor: shippingFee == 0 ? AppColors.success : null,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
_buildSummaryRow(
|
||||
'Giảm giá VIP:',
|
||||
'-${currencyFormatter.format(discount)}',
|
||||
valueColor: AppColors.success,
|
||||
),
|
||||
if (order.totalRemaining > 0) ...[
|
||||
_buildSummaryRow(
|
||||
'Còn lại:',
|
||||
currencyFormatter.format(order.totalRemaining),
|
||||
valueColor: AppColors.warning,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
|
||||
const Divider(height: 24),
|
||||
|
||||
_buildSummaryRow(
|
||||
'Tổng cộng:',
|
||||
currencyFormatter.format(total),
|
||||
currencyFormatter.format(order.grandTotal),
|
||||
isTotal: true,
|
||||
),
|
||||
|
||||
const Divider(height: 24),
|
||||
|
||||
// Payment Method
|
||||
// Payment Terms
|
||||
Row(
|
||||
children: [
|
||||
const FaIcon(
|
||||
@@ -942,22 +760,31 @@ class OrderDetailPage extends ConsumerWidget {
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
const Text(
|
||||
'Phương thức thanh toán:',
|
||||
'Điều khoản thanh toán:',
|
||||
style: TextStyle(fontSize: 14, color: AppColors.grey500),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
paymentMethod,
|
||||
paymentTerms.name,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.grey900,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
paymentTerms.description,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppColors.grey500,
|
||||
height: 1.4,
|
||||
),
|
||||
),
|
||||
|
||||
if (notes != null) ...[
|
||||
if (order.description.isNotEmpty) ...[
|
||||
const Divider(height: 24),
|
||||
|
||||
// Order Notes
|
||||
@@ -973,7 +800,7 @@ class OrderDetailPage extends ConsumerWidget {
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
notes,
|
||||
order.description,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
@@ -1019,56 +846,73 @@ class OrderDetailPage extends ConsumerWidget {
|
||||
);
|
||||
}
|
||||
|
||||
/// Get mock order data for development
|
||||
Map<String, dynamic> _getMockOrder() {
|
||||
return {
|
||||
'orderNumber': 'DH001234',
|
||||
'status': OrderStatus.processing,
|
||||
'statusHistory': [
|
||||
{
|
||||
'title': 'Đơn hàng được tạo',
|
||||
'date': '03/08/2023 - 09:30',
|
||||
'status': 'completed',
|
||||
},
|
||||
{
|
||||
'title': 'Đã xác nhận đơn hàng',
|
||||
'date': '03/08/2023 - 10:15',
|
||||
'status': 'completed',
|
||||
},
|
||||
{
|
||||
'title': 'Đang chuẩn bị hàng',
|
||||
'date': 'Đang thực hiện',
|
||||
'status': 'active',
|
||||
},
|
||||
{
|
||||
'title': 'Vận chuyển',
|
||||
'date': 'Dự kiến: 05/08/2023',
|
||||
'status': 'pending',
|
||||
},
|
||||
{
|
||||
'title': 'Giao hàng thành công',
|
||||
'date': 'Dự kiến: 07/08/2023',
|
||||
'status': 'pending',
|
||||
},
|
||||
],
|
||||
'deliveryMethod': 'Giao hàng tiêu chuẩn',
|
||||
'warehouseDate': DateTime(2023, 8, 5),
|
||||
'deliveryDate': DateTime(2023, 8, 7),
|
||||
'deliveryAddress':
|
||||
'123 Đường Lê Văn Lương, Phường Tân Hưng,\nQuận 7, TP. Hồ Chí Minh',
|
||||
'receiverName': 'Nguyễn Văn A',
|
||||
'receiverPhone': '0901234567',
|
||||
'customerName': 'Nguyễn Văn A',
|
||||
'customerPhone': '0901234567',
|
||||
'customerEmail': 'nguyenvana@email.com',
|
||||
'customerType': 'Khách VIP',
|
||||
'subtotal': 12900000.0,
|
||||
'shippingFee': 0.0,
|
||||
'discount': 129000.0,
|
||||
'total': 12771000.0,
|
||||
'paymentMethod': 'Chuyển khoản ngân hàng',
|
||||
'notes':
|
||||
'Giao hàng trong giờ hành chính. Vui lòng gọi trước 30 phút khi đến giao hàng.',
|
||||
};
|
||||
/// Build Action Buttons
|
||||
Widget _buildActionButtons(BuildContext context, OrderDetail orderDetail) {
|
||||
final shippingAddress = orderDetail.shippingAddress;
|
||||
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.white,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.1),
|
||||
blurRadius: 15,
|
||||
offset: const Offset(0, -4),
|
||||
),
|
||||
],
|
||||
),
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
spacing: 12,
|
||||
children: [
|
||||
Expanded(
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: () {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Gọi ${shippingAddress.phone}...'),
|
||||
),
|
||||
);
|
||||
},
|
||||
icon: const FaIcon(FontAwesomeIcons.phone, size: 18),
|
||||
label: const Text('Liên hệ'),
|
||||
style: OutlinedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
side: const BorderSide(
|
||||
color: AppColors.grey100,
|
||||
width: 2,
|
||||
),
|
||||
foregroundColor: AppColors.grey900,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Chức năng đang phát triển...'),
|
||||
),
|
||||
);
|
||||
},
|
||||
icon: const FaIcon(FontAwesomeIcons.penToSquare, size: 18),
|
||||
label: const Text('Cập nhật'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
backgroundColor: AppColors.primaryBlue,
|
||||
foregroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,4 +97,4 @@ final class OrderRepositoryProvider
|
||||
}
|
||||
}
|
||||
|
||||
String _$orderRepositoryHash() => r'985408a6667ab31427524f9b1981287c28f4f221';
|
||||
String _$orderRepositoryHash() => r'd1b811cb1849e44c48ce02d7bb620de1b0ccdfb8';
|
||||
|
||||
@@ -5,6 +5,7 @@ library;
|
||||
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:worker/features/orders/domain/entities/order.dart';
|
||||
import 'package:worker/features/orders/domain/entities/order_detail.dart';
|
||||
import 'package:worker/features/orders/domain/entities/order_status.dart';
|
||||
import 'package:worker/features/orders/presentation/providers/order_repository_provider.dart';
|
||||
|
||||
@@ -179,3 +180,12 @@ Future<List<OrderStatus>> orderStatusList(Ref ref) async {
|
||||
final repository = await ref.watch(orderRepositoryProvider.future);
|
||||
return await repository.getOrderStatusList();
|
||||
}
|
||||
|
||||
/// Order Detail Provider
|
||||
///
|
||||
/// Provides detailed order information by order ID.
|
||||
@riverpod
|
||||
Future<OrderDetail> orderDetail(Ref ref, String orderId) async {
|
||||
final repository = await ref.watch(orderRepositoryProvider.future);
|
||||
return await repository.getOrderDetail(orderId);
|
||||
}
|
||||
|
||||
@@ -398,3 +398,97 @@ final class OrderStatusListProvider
|
||||
}
|
||||
|
||||
String _$orderStatusListHash() => r'f005726ad238164f7e0dece62476b39fd762e933';
|
||||
|
||||
/// Order Detail Provider
|
||||
///
|
||||
/// Provides detailed order information by order ID.
|
||||
|
||||
@ProviderFor(orderDetail)
|
||||
const orderDetailProvider = OrderDetailFamily._();
|
||||
|
||||
/// Order Detail Provider
|
||||
///
|
||||
/// Provides detailed order information by order ID.
|
||||
|
||||
final class OrderDetailProvider
|
||||
extends
|
||||
$FunctionalProvider<
|
||||
AsyncValue<OrderDetail>,
|
||||
OrderDetail,
|
||||
FutureOr<OrderDetail>
|
||||
>
|
||||
with $FutureModifier<OrderDetail>, $FutureProvider<OrderDetail> {
|
||||
/// Order Detail Provider
|
||||
///
|
||||
/// Provides detailed order information by order ID.
|
||||
const OrderDetailProvider._({
|
||||
required OrderDetailFamily super.from,
|
||||
required String super.argument,
|
||||
}) : super(
|
||||
retry: null,
|
||||
name: r'orderDetailProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$orderDetailHash();
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return r'orderDetailProvider'
|
||||
''
|
||||
'($argument)';
|
||||
}
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$FutureProviderElement<OrderDetail> $createElement(
|
||||
$ProviderPointer pointer,
|
||||
) => $FutureProviderElement(pointer);
|
||||
|
||||
@override
|
||||
FutureOr<OrderDetail> create(Ref ref) {
|
||||
final argument = this.argument as String;
|
||||
return orderDetail(ref, argument);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is OrderDetailProvider && other.argument == argument;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return argument.hashCode;
|
||||
}
|
||||
}
|
||||
|
||||
String _$orderDetailHash() => r'628b9102b54579b8bba5f9135d875730cf2066c0';
|
||||
|
||||
/// Order Detail Provider
|
||||
///
|
||||
/// Provides detailed order information by order ID.
|
||||
|
||||
final class OrderDetailFamily extends $Family
|
||||
with $FunctionalFamilyOverride<FutureOr<OrderDetail>, String> {
|
||||
const OrderDetailFamily._()
|
||||
: super(
|
||||
retry: null,
|
||||
name: r'orderDetailProvider',
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
isAutoDispose: true,
|
||||
);
|
||||
|
||||
/// Order Detail Provider
|
||||
///
|
||||
/// Provides detailed order information by order ID.
|
||||
|
||||
OrderDetailProvider call(String orderId) =>
|
||||
OrderDetailProvider._(argument: orderId, from: this);
|
||||
|
||||
@override
|
||||
String toString() => r'orderDetailProvider';
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user