update order detail
This commit is contained in:
@@ -173,3 +173,98 @@ curl --location 'https://land.dbiz.com//api/method/building_material.building_ma
|
||||
--data '{
|
||||
"name" : "SAL-ORD-2025-00058-1"
|
||||
}'
|
||||
|
||||
#response order detail
|
||||
{
|
||||
"message": {
|
||||
"order": {
|
||||
"name": "SAL-ORD-2025-00107",
|
||||
"customer": "test - 1",
|
||||
"transaction_date": "2025-11-24",
|
||||
"delivery_date": "2025-11-24",
|
||||
"status": "Chờ phê duyệt",
|
||||
"status_color": "Warning",
|
||||
"total_qty": 2.56,
|
||||
"total": 3355443.2,
|
||||
"grand_total": 3355443.2,
|
||||
"total_remaining": 0,
|
||||
"description": "Order from mobile app",
|
||||
"contract_request": false,
|
||||
"ignore_pricing_rule": false,
|
||||
"rejection_reason": null,
|
||||
"is_allow_cancel": true
|
||||
},
|
||||
"billing_address": {
|
||||
"name": "phuoc-Billing-3",
|
||||
"address_title": "phuoc",
|
||||
"address_line1": "123 add dad",
|
||||
"phone": "0123123123",
|
||||
"email": "123@gmail.com",
|
||||
"fax": null,
|
||||
"tax_code": "064521840",
|
||||
"city_code": "19",
|
||||
"ward_code": "01936",
|
||||
"city_name": "Tỉnh Thái Nguyên",
|
||||
"ward_name": "Xã Nà Phặc",
|
||||
"is_allow_edit": true
|
||||
},
|
||||
"shipping_address": {
|
||||
"name": "phuoc-Billing-3",
|
||||
"address_title": "phuoc",
|
||||
"address_line1": "123 add dad",
|
||||
"phone": "0123123123",
|
||||
"email": "123@gmail.com",
|
||||
"fax": null,
|
||||
"tax_code": "064521840",
|
||||
"city_code": "19",
|
||||
"ward_code": "01936",
|
||||
"city_name": "Tỉnh Thái Nguyên",
|
||||
"ward_name": "Xã Nà Phặc",
|
||||
"is_allow_edit": true
|
||||
},
|
||||
"items": [
|
||||
{
|
||||
"name": "9crv0j6d4t",
|
||||
"item_code": "HOA E01",
|
||||
"item_name": "Hội An HOA E01",
|
||||
"description": "Hội An HOA E01",
|
||||
"qty_entered": 0.0,
|
||||
"qty_of_sm": 2.56,
|
||||
"qty_of_nos": 4.0,
|
||||
"conversion_factor": 1.5625,
|
||||
"price": 1310720.0,
|
||||
"total_amount": 3355443.2,
|
||||
"delivery_date": "2025-11-24",
|
||||
"thumbnail": "https://land.dbiz.com/files/HOA-E01-f1.jpg"
|
||||
}
|
||||
],
|
||||
"payment_terms": {
|
||||
"name": "Thanh toán hoàn toàn",
|
||||
"description": "Thanh toán ngay được chiết khấu 2%"
|
||||
},
|
||||
"timeline": [
|
||||
{
|
||||
"label": "Đã tạo đơn",
|
||||
"value": "2025-11-24 14:46:07",
|
||||
"status": "Success"
|
||||
},
|
||||
{
|
||||
"label": "Chờ phê duyệt",
|
||||
"value": null,
|
||||
"status": "Warning"
|
||||
},
|
||||
{
|
||||
"label": "Đơn đang xử lý",
|
||||
"value": "Prepare goods and transport",
|
||||
"status": "Secondary"
|
||||
},
|
||||
{
|
||||
"label": "Hoàn thành",
|
||||
"value": null,
|
||||
"status": "Secondary"
|
||||
}
|
||||
],
|
||||
"payments": [],
|
||||
"invoices": []
|
||||
}
|
||||
}
|
||||
502
lib/features/orders/data/models/order_detail_model.dart
Normal file
502
lib/features/orders/data/models/order_detail_model.dart
Normal file
@@ -0,0 +1,502 @@
|
||||
/// Order Detail Model
|
||||
///
|
||||
/// Data model for order detail API response.
|
||||
library;
|
||||
|
||||
import 'package:worker/features/orders/domain/entities/order_detail.dart';
|
||||
|
||||
/// Order Detail Model
|
||||
class OrderDetailModel {
|
||||
const OrderDetailModel({
|
||||
required this.order,
|
||||
required this.billingAddress,
|
||||
required this.shippingAddress,
|
||||
required this.items,
|
||||
required this.paymentTerms,
|
||||
required this.timeline,
|
||||
required this.payments,
|
||||
required this.invoices,
|
||||
});
|
||||
|
||||
final OrderDetailInfoModel order;
|
||||
final AddressInfoModel billingAddress;
|
||||
final AddressInfoModel shippingAddress;
|
||||
final List<OrderItemDetailModel> items;
|
||||
final PaymentTermsInfoModel paymentTerms;
|
||||
final List<TimelineItemModel> timeline;
|
||||
final List<dynamic> payments;
|
||||
final List<dynamic> invoices;
|
||||
|
||||
/// Create from JSON
|
||||
factory OrderDetailModel.fromJson(Map<String, dynamic> json) {
|
||||
return OrderDetailModel(
|
||||
order: OrderDetailInfoModel.fromJson(
|
||||
json['order'] as Map<String, dynamic>,
|
||||
),
|
||||
billingAddress: AddressInfoModel.fromJson(
|
||||
json['billing_address'] as Map<String, dynamic>,
|
||||
),
|
||||
shippingAddress: AddressInfoModel.fromJson(
|
||||
json['shipping_address'] as Map<String, dynamic>,
|
||||
),
|
||||
items: (json['items'] as List<dynamic>)
|
||||
.map((item) =>
|
||||
OrderItemDetailModel.fromJson(item as Map<String, dynamic>))
|
||||
.toList(),
|
||||
paymentTerms: PaymentTermsInfoModel.fromJson(
|
||||
json['payment_terms'] as Map<String, dynamic>,
|
||||
),
|
||||
timeline: (json['timeline'] as List<dynamic>)
|
||||
.map((item) =>
|
||||
TimelineItemModel.fromJson(item as Map<String, dynamic>))
|
||||
.toList(),
|
||||
payments: json['payments'] as List<dynamic>? ?? [],
|
||||
invoices: json['invoices'] as List<dynamic>? ?? [],
|
||||
);
|
||||
}
|
||||
|
||||
/// Convert to JSON
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'order': order.toJson(),
|
||||
'billing_address': billingAddress.toJson(),
|
||||
'shipping_address': shippingAddress.toJson(),
|
||||
'items': items.map((item) => item.toJson()).toList(),
|
||||
'payment_terms': paymentTerms.toJson(),
|
||||
'timeline': timeline.map((item) => item.toJson()).toList(),
|
||||
'payments': payments,
|
||||
'invoices': invoices,
|
||||
};
|
||||
}
|
||||
|
||||
/// Convert to domain entity
|
||||
OrderDetail toEntity() {
|
||||
return OrderDetail(
|
||||
order: order.toEntity(),
|
||||
billingAddress: billingAddress.toEntity(),
|
||||
shippingAddress: shippingAddress.toEntity(),
|
||||
items: items.map((item) => item.toEntity()).toList(),
|
||||
paymentTerms: paymentTerms.toEntity(),
|
||||
timeline: timeline.map((item) => item.toEntity()).toList(),
|
||||
payments: payments,
|
||||
invoices: invoices,
|
||||
);
|
||||
}
|
||||
|
||||
/// Create from domain entity
|
||||
factory OrderDetailModel.fromEntity(OrderDetail entity) {
|
||||
return OrderDetailModel(
|
||||
order: OrderDetailInfoModel.fromEntity(entity.order),
|
||||
billingAddress: AddressInfoModel.fromEntity(entity.billingAddress),
|
||||
shippingAddress: AddressInfoModel.fromEntity(entity.shippingAddress),
|
||||
items: entity.items
|
||||
.map((item) => OrderItemDetailModel.fromEntity(item))
|
||||
.toList(),
|
||||
paymentTerms: PaymentTermsInfoModel.fromEntity(entity.paymentTerms),
|
||||
timeline: entity.timeline
|
||||
.map((item) => TimelineItemModel.fromEntity(item))
|
||||
.toList(),
|
||||
payments: entity.payments,
|
||||
invoices: entity.invoices,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Order Detail Info Model
|
||||
class OrderDetailInfoModel {
|
||||
const OrderDetailInfoModel({
|
||||
required this.name,
|
||||
required this.customer,
|
||||
required this.transactionDate,
|
||||
required this.deliveryDate,
|
||||
required this.status,
|
||||
required this.statusColor,
|
||||
required this.totalQty,
|
||||
required this.total,
|
||||
required this.grandTotal,
|
||||
required this.totalRemaining,
|
||||
required this.description,
|
||||
required this.contractRequest,
|
||||
required this.ignorePricingRule,
|
||||
this.rejectionReason,
|
||||
required this.isAllowCancel,
|
||||
});
|
||||
|
||||
final String name;
|
||||
final String customer;
|
||||
final String transactionDate;
|
||||
final String deliveryDate;
|
||||
final String status;
|
||||
final String statusColor;
|
||||
final double totalQty;
|
||||
final double total;
|
||||
final double grandTotal;
|
||||
final double totalRemaining;
|
||||
final String description;
|
||||
final bool contractRequest;
|
||||
final bool ignorePricingRule;
|
||||
final String? rejectionReason;
|
||||
final bool isAllowCancel;
|
||||
|
||||
factory OrderDetailInfoModel.fromJson(Map<String, dynamic> json) {
|
||||
return OrderDetailInfoModel(
|
||||
name: json['name'] as String,
|
||||
customer: json['customer'] as String,
|
||||
transactionDate: json['transaction_date'] as String,
|
||||
deliveryDate: json['delivery_date'] as String,
|
||||
status: json['status'] as String,
|
||||
statusColor: json['status_color'] as String,
|
||||
totalQty: (json['total_qty'] as num).toDouble(),
|
||||
total: (json['total'] as num).toDouble(),
|
||||
grandTotal: (json['grand_total'] as num).toDouble(),
|
||||
totalRemaining: (json['total_remaining'] as num).toDouble(),
|
||||
description: json['description'] as String,
|
||||
contractRequest: json['contract_request'] as bool,
|
||||
ignorePricingRule: json['ignore_pricing_rule'] as bool,
|
||||
rejectionReason: json['rejection_reason'] as String?,
|
||||
isAllowCancel: json['is_allow_cancel'] as bool,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'name': name,
|
||||
'customer': customer,
|
||||
'transaction_date': transactionDate,
|
||||
'delivery_date': deliveryDate,
|
||||
'status': status,
|
||||
'status_color': statusColor,
|
||||
'total_qty': totalQty,
|
||||
'total': total,
|
||||
'grand_total': grandTotal,
|
||||
'total_remaining': totalRemaining,
|
||||
'description': description,
|
||||
'contract_request': contractRequest,
|
||||
'ignore_pricing_rule': ignorePricingRule,
|
||||
'rejection_reason': rejectionReason,
|
||||
'is_allow_cancel': isAllowCancel,
|
||||
};
|
||||
}
|
||||
|
||||
OrderDetailInfo toEntity() {
|
||||
return OrderDetailInfo(
|
||||
name: name,
|
||||
customer: customer,
|
||||
transactionDate: transactionDate,
|
||||
deliveryDate: deliveryDate,
|
||||
status: status,
|
||||
statusColor: statusColor,
|
||||
totalQty: totalQty,
|
||||
total: total,
|
||||
grandTotal: grandTotal,
|
||||
totalRemaining: totalRemaining,
|
||||
description: description,
|
||||
contractRequest: contractRequest,
|
||||
ignorePricingRule: ignorePricingRule,
|
||||
rejectionReason: rejectionReason,
|
||||
isAllowCancel: isAllowCancel,
|
||||
);
|
||||
}
|
||||
|
||||
factory OrderDetailInfoModel.fromEntity(OrderDetailInfo entity) {
|
||||
return OrderDetailInfoModel(
|
||||
name: entity.name,
|
||||
customer: entity.customer,
|
||||
transactionDate: entity.transactionDate,
|
||||
deliveryDate: entity.deliveryDate,
|
||||
status: entity.status,
|
||||
statusColor: entity.statusColor,
|
||||
totalQty: entity.totalQty,
|
||||
total: entity.total,
|
||||
grandTotal: entity.grandTotal,
|
||||
totalRemaining: entity.totalRemaining,
|
||||
description: entity.description,
|
||||
contractRequest: entity.contractRequest,
|
||||
ignorePricingRule: entity.ignorePricingRule,
|
||||
rejectionReason: entity.rejectionReason,
|
||||
isAllowCancel: entity.isAllowCancel,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 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({
|
||||
required this.name,
|
||||
required this.itemCode,
|
||||
required this.itemName,
|
||||
required this.description,
|
||||
required this.qtyEntered,
|
||||
required this.qtyOfSm,
|
||||
required this.qtyOfNos,
|
||||
required this.conversionFactor,
|
||||
required this.price,
|
||||
required this.totalAmount,
|
||||
required this.deliveryDate,
|
||||
this.thumbnail,
|
||||
});
|
||||
|
||||
final String name;
|
||||
final String itemCode;
|
||||
final String itemName;
|
||||
final String description;
|
||||
final double qtyEntered;
|
||||
final double qtyOfSm;
|
||||
final double qtyOfNos;
|
||||
final double conversionFactor;
|
||||
final double price;
|
||||
final double totalAmount;
|
||||
final String deliveryDate;
|
||||
final String? thumbnail;
|
||||
|
||||
factory OrderItemDetailModel.fromJson(Map<String, dynamic> json) {
|
||||
return OrderItemDetailModel(
|
||||
name: json['name'] as String,
|
||||
itemCode: json['item_code'] as String,
|
||||
itemName: json['item_name'] as String,
|
||||
description: json['description'] as String,
|
||||
qtyEntered: (json['qty_entered'] as num).toDouble(),
|
||||
qtyOfSm: (json['qty_of_sm'] as num).toDouble(),
|
||||
qtyOfNos: (json['qty_of_nos'] as num).toDouble(),
|
||||
conversionFactor: (json['conversion_factor'] as num).toDouble(),
|
||||
price: (json['price'] as num).toDouble(),
|
||||
totalAmount: (json['total_amount'] as num).toDouble(),
|
||||
deliveryDate: json['delivery_date'] as String,
|
||||
thumbnail: json['thumbnail'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'name': name,
|
||||
'item_code': itemCode,
|
||||
'item_name': itemName,
|
||||
'description': description,
|
||||
'qty_entered': qtyEntered,
|
||||
'qty_of_sm': qtyOfSm,
|
||||
'qty_of_nos': qtyOfNos,
|
||||
'conversion_factor': conversionFactor,
|
||||
'price': price,
|
||||
'total_amount': totalAmount,
|
||||
'delivery_date': deliveryDate,
|
||||
'thumbnail': thumbnail,
|
||||
};
|
||||
}
|
||||
|
||||
OrderItemDetail toEntity() {
|
||||
return OrderItemDetail(
|
||||
name: name,
|
||||
itemCode: itemCode,
|
||||
itemName: itemName,
|
||||
description: description,
|
||||
qtyEntered: qtyEntered,
|
||||
qtyOfSm: qtyOfSm,
|
||||
qtyOfNos: qtyOfNos,
|
||||
conversionFactor: conversionFactor,
|
||||
price: price,
|
||||
totalAmount: totalAmount,
|
||||
deliveryDate: deliveryDate,
|
||||
thumbnail: thumbnail,
|
||||
);
|
||||
}
|
||||
|
||||
factory OrderItemDetailModel.fromEntity(OrderItemDetail entity) {
|
||||
return OrderItemDetailModel(
|
||||
name: entity.name,
|
||||
itemCode: entity.itemCode,
|
||||
itemName: entity.itemName,
|
||||
description: entity.description,
|
||||
qtyEntered: entity.qtyEntered,
|
||||
qtyOfSm: entity.qtyOfSm,
|
||||
qtyOfNos: entity.qtyOfNos,
|
||||
conversionFactor: entity.conversionFactor,
|
||||
price: entity.price,
|
||||
totalAmount: entity.totalAmount,
|
||||
deliveryDate: entity.deliveryDate,
|
||||
thumbnail: entity.thumbnail,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Payment Terms Info Model
|
||||
class PaymentTermsInfoModel {
|
||||
const PaymentTermsInfoModel({
|
||||
required this.name,
|
||||
required this.description,
|
||||
});
|
||||
|
||||
final String name;
|
||||
final String description;
|
||||
|
||||
factory PaymentTermsInfoModel.fromJson(Map<String, dynamic> json) {
|
||||
return PaymentTermsInfoModel(
|
||||
name: json['name'] as String,
|
||||
description: json['description'] as String,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'name': name,
|
||||
'description': description,
|
||||
};
|
||||
}
|
||||
|
||||
PaymentTermsInfo toEntity() {
|
||||
return PaymentTermsInfo(
|
||||
name: name,
|
||||
description: description,
|
||||
);
|
||||
}
|
||||
|
||||
factory PaymentTermsInfoModel.fromEntity(PaymentTermsInfo entity) {
|
||||
return PaymentTermsInfoModel(
|
||||
name: entity.name,
|
||||
description: entity.description,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Timeline Item Model
|
||||
class TimelineItemModel {
|
||||
const TimelineItemModel({
|
||||
required this.label,
|
||||
this.value,
|
||||
required this.status,
|
||||
});
|
||||
|
||||
final String label;
|
||||
final String? value;
|
||||
final String status;
|
||||
|
||||
factory TimelineItemModel.fromJson(Map<String, dynamic> json) {
|
||||
return TimelineItemModel(
|
||||
label: json['label'] as String,
|
||||
value: json['value'] as String?,
|
||||
status: json['status'] as String,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'label': label,
|
||||
'value': value,
|
||||
'status': status,
|
||||
};
|
||||
}
|
||||
|
||||
TimelineItem toEntity() {
|
||||
return TimelineItem(
|
||||
label: label,
|
||||
value: value,
|
||||
status: status,
|
||||
);
|
||||
}
|
||||
|
||||
factory TimelineItemModel.fromEntity(TimelineItem entity) {
|
||||
return TimelineItemModel(
|
||||
label: entity.label,
|
||||
value: entity.value,
|
||||
status: entity.status,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -5,8 +5,10 @@ library;
|
||||
|
||||
import 'package:worker/features/orders/data/datasources/order_remote_datasource.dart';
|
||||
import 'package:worker/features/orders/data/datasources/order_status_local_datasource.dart';
|
||||
import 'package:worker/features/orders/data/models/order_detail_model.dart';
|
||||
import 'package:worker/features/orders/data/models/order_model.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/domain/entities/payment_term.dart';
|
||||
import 'package:worker/features/orders/domain/repositories/order_repository.dart';
|
||||
@@ -40,6 +42,17 @@ class OrderRepositoryImpl implements OrderRepository {
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<OrderDetail> getOrderDetail(String orderId) async {
|
||||
try {
|
||||
final detailData = await _remoteDataSource.getOrderDetail(orderId);
|
||||
// Convert JSON → Model → Entity
|
||||
return OrderDetailModel.fromJson(detailData).toEntity();
|
||||
} catch (e) {
|
||||
throw Exception('Failed to get order detail: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<OrderStatus>> getOrderStatusList() async {
|
||||
try {
|
||||
|
||||
221
lib/features/orders/domain/entities/order_detail.dart
Normal file
221
lib/features/orders/domain/entities/order_detail.dart
Normal file
@@ -0,0 +1,221 @@
|
||||
/// Order Detail Entity
|
||||
///
|
||||
/// Complete order detail information including addresses, items, timeline, etc.
|
||||
library;
|
||||
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
/// Order Detail Entity
|
||||
class OrderDetail extends Equatable {
|
||||
const OrderDetail({
|
||||
required this.order,
|
||||
required this.billingAddress,
|
||||
required this.shippingAddress,
|
||||
required this.items,
|
||||
required this.paymentTerms,
|
||||
required this.timeline,
|
||||
required this.payments,
|
||||
required this.invoices,
|
||||
});
|
||||
|
||||
final OrderDetailInfo order;
|
||||
final AddressInfo billingAddress;
|
||||
final AddressInfo shippingAddress;
|
||||
final List<OrderItemDetail> items;
|
||||
final PaymentTermsInfo paymentTerms;
|
||||
final List<TimelineItem> timeline;
|
||||
final List<dynamic> payments; // Payment entities can be added later
|
||||
final List<dynamic> invoices; // Invoice entities can be added later
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
order,
|
||||
billingAddress,
|
||||
shippingAddress,
|
||||
items,
|
||||
paymentTerms,
|
||||
timeline,
|
||||
payments,
|
||||
invoices,
|
||||
];
|
||||
}
|
||||
|
||||
/// Order Detail Info
|
||||
class OrderDetailInfo extends Equatable {
|
||||
const OrderDetailInfo({
|
||||
required this.name,
|
||||
required this.customer,
|
||||
required this.transactionDate,
|
||||
required this.deliveryDate,
|
||||
required this.status,
|
||||
required this.statusColor,
|
||||
required this.totalQty,
|
||||
required this.total,
|
||||
required this.grandTotal,
|
||||
required this.totalRemaining,
|
||||
required this.description,
|
||||
required this.contractRequest,
|
||||
required this.ignorePricingRule,
|
||||
this.rejectionReason,
|
||||
required this.isAllowCancel,
|
||||
});
|
||||
|
||||
final String name;
|
||||
final String customer;
|
||||
final String transactionDate;
|
||||
final String deliveryDate;
|
||||
final String status;
|
||||
final String statusColor;
|
||||
final double totalQty;
|
||||
final double total;
|
||||
final double grandTotal;
|
||||
final double totalRemaining;
|
||||
final String description;
|
||||
final bool contractRequest;
|
||||
final bool ignorePricingRule;
|
||||
final String? rejectionReason;
|
||||
final bool isAllowCancel;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
name,
|
||||
customer,
|
||||
transactionDate,
|
||||
deliveryDate,
|
||||
status,
|
||||
statusColor,
|
||||
totalQty,
|
||||
total,
|
||||
grandTotal,
|
||||
totalRemaining,
|
||||
description,
|
||||
contractRequest,
|
||||
ignorePricingRule,
|
||||
rejectionReason,
|
||||
isAllowCancel,
|
||||
];
|
||||
}
|
||||
|
||||
/// 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({
|
||||
required this.name,
|
||||
required this.itemCode,
|
||||
required this.itemName,
|
||||
required this.description,
|
||||
required this.qtyEntered,
|
||||
required this.qtyOfSm,
|
||||
required this.qtyOfNos,
|
||||
required this.conversionFactor,
|
||||
required this.price,
|
||||
required this.totalAmount,
|
||||
required this.deliveryDate,
|
||||
this.thumbnail,
|
||||
});
|
||||
|
||||
final String name;
|
||||
final String itemCode;
|
||||
final String itemName;
|
||||
final String description;
|
||||
final double qtyEntered;
|
||||
final double qtyOfSm;
|
||||
final double qtyOfNos;
|
||||
final double conversionFactor;
|
||||
final double price;
|
||||
final double totalAmount;
|
||||
final String deliveryDate;
|
||||
final String? thumbnail;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
name,
|
||||
itemCode,
|
||||
itemName,
|
||||
description,
|
||||
qtyEntered,
|
||||
qtyOfSm,
|
||||
qtyOfNos,
|
||||
conversionFactor,
|
||||
price,
|
||||
totalAmount,
|
||||
deliveryDate,
|
||||
thumbnail,
|
||||
];
|
||||
}
|
||||
|
||||
/// Payment Terms Info
|
||||
class PaymentTermsInfo extends Equatable {
|
||||
const PaymentTermsInfo({
|
||||
required this.name,
|
||||
required this.description,
|
||||
});
|
||||
|
||||
final String name;
|
||||
final String description;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [name, description];
|
||||
}
|
||||
|
||||
/// Timeline Item
|
||||
class TimelineItem extends Equatable {
|
||||
const TimelineItem({
|
||||
required this.label,
|
||||
this.value,
|
||||
required this.status,
|
||||
});
|
||||
|
||||
final String label;
|
||||
final String? value;
|
||||
final String status; // Success, Warning, Secondary, etc.
|
||||
|
||||
@override
|
||||
List<Object?> get props => [label, value, status];
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
library;
|
||||
|
||||
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/domain/entities/payment_term.dart';
|
||||
|
||||
@@ -15,6 +16,9 @@ abstract class OrderRepository {
|
||||
int limitPageLength = 0,
|
||||
});
|
||||
|
||||
/// Get order detail by ID
|
||||
Future<OrderDetail> getOrderDetail(String orderId);
|
||||
|
||||
/// Get list of available order statuses
|
||||
Future<List<OrderStatus>> getOrderStatusList();
|
||||
|
||||
|
||||
@@ -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,49 +70,28 @@ class OrderDetailPage extends ConsumerWidget {
|
||||
foregroundColor: AppColors.grey900,
|
||||
centerTitle: false,
|
||||
),
|
||||
body: Stack(
|
||||
body: orderDetailAsync.when(
|
||||
data: (orderDetail) {
|
||||
return 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>>,
|
||||
),
|
||||
_buildStatusTimelineCard(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,
|
||||
),
|
||||
// Delivery/Address Information Card
|
||||
_buildAddressInfoCard(orderDetail),
|
||||
|
||||
// Customer Information Card
|
||||
_buildCustomerInfoCard(
|
||||
mockOrder['customerName']! as String,
|
||||
mockOrder['customerPhone']! as String,
|
||||
mockOrder['customerEmail']! as String,
|
||||
mockOrder['customerType']! as String,
|
||||
),
|
||||
_buildCustomerInfoCard(orderDetail),
|
||||
|
||||
// Products List Card
|
||||
_buildProductsListCard(),
|
||||
_buildProductsListCard(orderDetail),
|
||||
|
||||
// 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?,
|
||||
),
|
||||
_buildOrderSummaryCard(orderDetail),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -121,84 +101,67 @@ class OrderDetailPage extends ConsumerWidget {
|
||||
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),
|
||||
child: _buildActionButtons(context, orderDetail),
|
||||
),
|
||||
],
|
||||
),
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
spacing: 12,
|
||||
);
|
||||
},
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error: (error, stack) => Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Expanded(
|
||||
child: OutlinedButton.icon(
|
||||
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: () {
|
||||
// TODO: Implement contact customer
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Gọi điện cho khách hàng...'),
|
||||
),
|
||||
);
|
||||
ref.invalidate(orderDetailProvider(orderId));
|
||||
},
|
||||
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'),
|
||||
icon: const FaIcon(FontAwesomeIcons.arrowsRotate, size: 16),
|
||||
label: const Text('Thử lại'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
backgroundColor: AppColors.primaryBlue,
|
||||
foregroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
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',
|
||||
'#${order.name}',
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColors.primaryBlue,
|
||||
),
|
||||
),
|
||||
_buildStatusBadge(currentStatus),
|
||||
],
|
||||
),
|
||||
|
||||
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,25 +209,25 @@ 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':
|
||||
if (statusColor == StatusColor.success) {
|
||||
iconColor = Colors.white;
|
||||
iconBgColor = AppColors.success;
|
||||
iconBgColor = statusColor.color;
|
||||
iconData = FontAwesomeIcons.check;
|
||||
break;
|
||||
case 'active':
|
||||
} else if (statusColor == StatusColor.warning) {
|
||||
iconColor = Colors.white;
|
||||
iconBgColor = AppColors.warning;
|
||||
iconBgColor = statusColor.color;
|
||||
iconData = FontAwesomeIcons.gear;
|
||||
break;
|
||||
default: // pending
|
||||
} else {
|
||||
// Secondary or other
|
||||
iconColor = AppColors.grey500;
|
||||
iconBgColor = AppColors.grey100;
|
||||
iconData = _getIconForTitle(title);
|
||||
@@ -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,6 +563,35 @@ class OrderDetailPage extends ConsumerWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Product Image
|
||||
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,
|
||||
@@ -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),
|
||||
|
||||
if (order.totalRemaining > 0) ...[
|
||||
_buildSummaryRow(
|
||||
'Phí vận chuyển:',
|
||||
shippingFee == 0
|
||||
? 'Miễn phí'
|
||||
: currencyFormatter.format(shippingFee),
|
||||
valueColor: shippingFee == 0 ? AppColors.success : null,
|
||||
'Còn lại:',
|
||||
currencyFormatter.format(order.totalRemaining),
|
||||
valueColor: AppColors.warning,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
_buildSummaryRow(
|
||||
'Giảm giá VIP:',
|
||||
'-${currencyFormatter.format(discount)}',
|
||||
valueColor: AppColors.success,
|
||||
),
|
||||
],
|
||||
|
||||
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',
|
||||
},
|
||||
/// 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),
|
||||
),
|
||||
],
|
||||
'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.',
|
||||
};
|
||||
),
|
||||
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