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 '{
|
--data '{
|
||||||
"name" : "SAL-ORD-2025-00058-1"
|
"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_remote_datasource.dart';
|
||||||
import 'package:worker/features/orders/data/datasources/order_status_local_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/data/models/order_model.dart';
|
||||||
import 'package:worker/features/orders/domain/entities/order.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/order_status.dart';
|
||||||
import 'package:worker/features/orders/domain/entities/payment_term.dart';
|
import 'package:worker/features/orders/domain/entities/payment_term.dart';
|
||||||
import 'package:worker/features/orders/domain/repositories/order_repository.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
|
@override
|
||||||
Future<List<OrderStatus>> getOrderStatusList() async {
|
Future<List<OrderStatus>> getOrderStatusList() async {
|
||||||
try {
|
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;
|
library;
|
||||||
|
|
||||||
import 'package:worker/features/orders/domain/entities/order.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/order_status.dart';
|
||||||
import 'package:worker/features/orders/domain/entities/payment_term.dart';
|
import 'package:worker/features/orders/domain/entities/payment_term.dart';
|
||||||
|
|
||||||
@@ -15,6 +16,9 @@ abstract class OrderRepository {
|
|||||||
int limitPageLength = 0,
|
int limitPageLength = 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/// Get order detail by ID
|
||||||
|
Future<OrderDetail> getOrderDetail(String orderId);
|
||||||
|
|
||||||
/// Get list of available order statuses
|
/// Get list of available order statuses
|
||||||
Future<List<OrderStatus>> getOrderStatusList();
|
Future<List<OrderStatus>> getOrderStatusList();
|
||||||
|
|
||||||
|
|||||||
@@ -3,14 +3,17 @@
|
|||||||
/// Displays detailed order information including status timeline, delivery info, and products.
|
/// Displays detailed order information including status timeline, delivery info, and products.
|
||||||
library;
|
library;
|
||||||
|
|
||||||
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:worker/core/constants/ui_constants.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/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
|
/// Order Detail Page
|
||||||
///
|
///
|
||||||
@@ -27,9 +30,7 @@ class OrderDetailPage extends ConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
// TODO: Replace with actual order data from provider
|
final orderDetailAsync = ref.watch(orderDetailProvider(orderId));
|
||||||
// For now using mock data based on HTML reference
|
|
||||||
final mockOrder = _getMockOrder();
|
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: const Color(0xFFF4F6F8),
|
backgroundColor: const Color(0xFFF4F6F8),
|
||||||
@@ -69,49 +70,28 @@ class OrderDetailPage extends ConsumerWidget {
|
|||||||
foregroundColor: AppColors.grey900,
|
foregroundColor: AppColors.grey900,
|
||||||
centerTitle: false,
|
centerTitle: false,
|
||||||
),
|
),
|
||||||
body: Stack(
|
body: orderDetailAsync.when(
|
||||||
|
data: (orderDetail) {
|
||||||
|
return Stack(
|
||||||
children: [
|
children: [
|
||||||
SingleChildScrollView(
|
SingleChildScrollView(
|
||||||
padding: const EdgeInsets.only(bottom: 100),
|
padding: const EdgeInsets.only(bottom: 100),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
// Status Timeline Card
|
// Status Timeline Card
|
||||||
_buildStatusTimelineCard(
|
_buildStatusTimelineCard(orderDetail),
|
||||||
mockOrder['orderNumber']! as String,
|
|
||||||
mockOrder['status']! as OrderStatus,
|
|
||||||
mockOrder['statusHistory']! as List<Map<String, dynamic>>,
|
|
||||||
),
|
|
||||||
|
|
||||||
// Delivery Information Card
|
// Delivery/Address Information Card
|
||||||
_buildDeliveryInfoCard(
|
_buildAddressInfoCard(orderDetail),
|
||||||
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
|
// Customer Information Card
|
||||||
_buildCustomerInfoCard(
|
_buildCustomerInfoCard(orderDetail),
|
||||||
mockOrder['customerName']! as String,
|
|
||||||
mockOrder['customerPhone']! as String,
|
|
||||||
mockOrder['customerEmail']! as String,
|
|
||||||
mockOrder['customerType']! as String,
|
|
||||||
),
|
|
||||||
|
|
||||||
// Products List Card
|
// Products List Card
|
||||||
_buildProductsListCard(),
|
_buildProductsListCard(orderDetail),
|
||||||
|
|
||||||
// Order Summary Card
|
// Order Summary Card
|
||||||
_buildOrderSummaryCard(
|
_buildOrderSummaryCard(orderDetail),
|
||||||
mockOrder['subtotal']! as double,
|
|
||||||
mockOrder['shippingFee']! as double,
|
|
||||||
mockOrder['discount']! as double,
|
|
||||||
mockOrder['total']! as double,
|
|
||||||
mockOrder['paymentMethod']! as String,
|
|
||||||
mockOrder['notes'] as String?,
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -121,84 +101,67 @@ class OrderDetailPage extends ConsumerWidget {
|
|||||||
bottom: 0,
|
bottom: 0,
|
||||||
left: 0,
|
left: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
child: Container(
|
child: _buildActionButtons(context, orderDetail),
|
||||||
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(
|
loading: () => const Center(child: CircularProgressIndicator()),
|
||||||
spacing: 12,
|
error: (error, stack) => Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
const FaIcon(
|
||||||
child: OutlinedButton.icon(
|
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: () {
|
onPressed: () {
|
||||||
// TODO: Implement contact customer
|
ref.invalidate(orderDetailProvider(orderId));
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
const SnackBar(
|
|
||||||
content: Text('Gọi điện cho khách hàng...'),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
icon: const FaIcon(FontAwesomeIcons.phone, size: 18),
|
icon: const FaIcon(FontAwesomeIcons.arrowsRotate, size: 16),
|
||||||
label: const Text('Liên hệ khách hàng'),
|
label: const Text('Thử lại'),
|
||||||
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(
|
style: ElevatedButton.styleFrom(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
|
||||||
backgroundColor: AppColors.primaryBlue,
|
backgroundColor: AppColors.primaryBlue,
|
||||||
foregroundColor: Colors.white,
|
foregroundColor: Colors.white,
|
||||||
elevation: 0,
|
padding: const EdgeInsets.symmetric(
|
||||||
shape: RoundedRectangleBorder(
|
horizontal: 24,
|
||||||
borderRadius: BorderRadius.circular(8),
|
vertical: 12,
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Build Status Timeline Card
|
/// Build Status Timeline Card
|
||||||
Widget _buildStatusTimelineCard(
|
Widget _buildStatusTimelineCard(OrderDetail orderDetail) {
|
||||||
String orderNumber,
|
final order = orderDetail.order;
|
||||||
OrderStatus currentStatus,
|
final timeline = orderDetail.timeline;
|
||||||
List<Map<String, dynamic>> statusHistory,
|
|
||||||
) {
|
|
||||||
return Card(
|
return Card(
|
||||||
margin: const EdgeInsets.all(16),
|
margin: const EdgeInsets.all(16),
|
||||||
elevation: 1,
|
elevation: 1,
|
||||||
@@ -209,33 +172,30 @@ class OrderDetailPage extends ConsumerWidget {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
// Order Number and Status Badge
|
// Order Number and Status Badge
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Text(
|
Text(
|
||||||
'#$orderNumber',
|
'#${order.name}',
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 20,
|
fontSize: 20,
|
||||||
fontWeight: FontWeight.w700,
|
fontWeight: FontWeight.w700,
|
||||||
color: AppColors.primaryBlue,
|
color: AppColors.primaryBlue,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
_buildStatusBadge(currentStatus),
|
|
||||||
],
|
const SizedBox(height: 12,),
|
||||||
),
|
_buildStatusBadge(order.status, order.statusColor),
|
||||||
|
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
// Status Timeline
|
// Status Timeline
|
||||||
...statusHistory.asMap().entries.map((entry) {
|
...timeline.asMap().entries.map((entry) {
|
||||||
final index = entry.key;
|
final index = entry.key;
|
||||||
final item = entry.value;
|
final item = entry.value;
|
||||||
final isLast = index == statusHistory.length - 1;
|
final isLast = index == timeline.length - 1;
|
||||||
|
|
||||||
return _buildTimelineItem(
|
return _buildTimelineItem(
|
||||||
title: item['title']! as String,
|
title: item.label,
|
||||||
date: item['date'] as String?,
|
date: item.value,
|
||||||
status: item['status']! as String,
|
status: item.status,
|
||||||
isLast: isLast,
|
isLast: isLast,
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
@@ -249,25 +209,25 @@ class OrderDetailPage extends ConsumerWidget {
|
|||||||
Widget _buildTimelineItem({
|
Widget _buildTimelineItem({
|
||||||
required String title,
|
required String title,
|
||||||
String? date,
|
String? date,
|
||||||
required String status, // 'completed', 'active', 'pending'
|
required String status, // 'Success', 'Warning', 'Secondary', etc.
|
||||||
required bool isLast,
|
required bool isLast,
|
||||||
}) {
|
}) {
|
||||||
|
final statusColor = StatusColor.fromString(status) ?? StatusColor.secondary;
|
||||||
|
|
||||||
Color iconColor;
|
Color iconColor;
|
||||||
Color iconBgColor;
|
Color iconBgColor;
|
||||||
IconData iconData;
|
IconData iconData;
|
||||||
|
|
||||||
switch (status) {
|
if (statusColor == StatusColor.success) {
|
||||||
case 'completed':
|
|
||||||
iconColor = Colors.white;
|
iconColor = Colors.white;
|
||||||
iconBgColor = AppColors.success;
|
iconBgColor = statusColor.color;
|
||||||
iconData = FontAwesomeIcons.check;
|
iconData = FontAwesomeIcons.check;
|
||||||
break;
|
} else if (statusColor == StatusColor.warning) {
|
||||||
case 'active':
|
|
||||||
iconColor = Colors.white;
|
iconColor = Colors.white;
|
||||||
iconBgColor = AppColors.warning;
|
iconBgColor = statusColor.color;
|
||||||
iconData = FontAwesomeIcons.gear;
|
iconData = FontAwesomeIcons.gear;
|
||||||
break;
|
} else {
|
||||||
default: // pending
|
// Secondary or other
|
||||||
iconColor = AppColors.grey500;
|
iconColor = AppColors.grey500;
|
||||||
iconBgColor = AppColors.grey100;
|
iconBgColor = AppColors.grey100;
|
||||||
iconData = _getIconForTitle(title);
|
iconData = _getIconForTitle(title);
|
||||||
@@ -286,7 +246,7 @@ class OrderDetailPage extends ConsumerWidget {
|
|||||||
color: iconBgColor,
|
color: iconBgColor,
|
||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
),
|
),
|
||||||
child: FaIcon(iconData, size: 10, color: iconColor),
|
child: Center(child: FaIcon(iconData, size: 10, color: iconColor)),
|
||||||
),
|
),
|
||||||
if (!isLast)
|
if (!isLast)
|
||||||
Container(
|
Container(
|
||||||
@@ -342,85 +302,34 @@ class OrderDetailPage extends ConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Build Status Badge
|
/// Build Status Badge
|
||||||
Widget _buildStatusBadge(OrderStatus status) {
|
Widget _buildStatusBadge(String status, String statusColorName) {
|
||||||
|
final statusColor = StatusColor.fromString(statusColorName) ?? StatusColor.secondary;
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: _getStatusColor(status).withValues(alpha: 0.1),
|
color: statusColor.light,
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: _getStatusColor(status).withValues(alpha: 0.3),
|
color: statusColor.border,
|
||||||
width: 1,
|
width: 1,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
_getStatusText(status),
|
status,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
color: _getStatusColor(status),
|
color: statusColor.color,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get status color
|
/// Build Address Info Card
|
||||||
Color _getStatusColor(OrderStatus status) {
|
Widget _buildAddressInfoCard(OrderDetail orderDetail) {
|
||||||
switch (status) {
|
final order = orderDetail.order;
|
||||||
case OrderStatus.draft:
|
final shippingAddress = orderDetail.shippingAddress;
|
||||||
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,
|
|
||||||
) {
|
|
||||||
final dateFormatter = DateFormat('dd/MM/yyyy');
|
final dateFormatter = DateFormat('dd/MM/yyyy');
|
||||||
|
|
||||||
return Card(
|
return Card(
|
||||||
@@ -453,72 +362,11 @@ class OrderDetailPage extends ConsumerWidget {
|
|||||||
|
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
// Delivery Method
|
// Delivery Date
|
||||||
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
|
|
||||||
_buildInfoRow(
|
_buildInfoRow(
|
||||||
icon: FontAwesomeIcons.calendar,
|
icon: FontAwesomeIcons.calendar,
|
||||||
label: 'Ngày xuất kho',
|
label: 'Ngày giao hàng',
|
||||||
value: dateFormatter.format(warehouseDate),
|
value: dateFormatter.format(DateTime.parse(order.deliveryDate)),
|
||||||
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',
|
|
||||||
),
|
),
|
||||||
|
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
@@ -526,7 +374,8 @@ class OrderDetailPage extends ConsumerWidget {
|
|||||||
_buildInfoRow(
|
_buildInfoRow(
|
||||||
icon: FontAwesomeIcons.locationDot,
|
icon: FontAwesomeIcons.locationDot,
|
||||||
label: 'Địa chỉ giao hàng',
|
label: 'Địa chỉ giao hàng',
|
||||||
value: deliveryAddress,
|
value:
|
||||||
|
'${shippingAddress.addressLine1}\n${shippingAddress.wardName}, ${shippingAddress.cityName}',
|
||||||
),
|
),
|
||||||
|
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
@@ -534,7 +383,7 @@ class OrderDetailPage extends ConsumerWidget {
|
|||||||
_buildInfoRow(
|
_buildInfoRow(
|
||||||
icon: FontAwesomeIcons.user,
|
icon: FontAwesomeIcons.user,
|
||||||
label: 'Người nhận',
|
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
|
/// Build Customer Info Card
|
||||||
Widget _buildCustomerInfoCard(
|
Widget _buildCustomerInfoCard(OrderDetail orderDetail) {
|
||||||
String customerName,
|
final order = orderDetail.order;
|
||||||
String customerPhone,
|
final billingAddress = orderDetail.billingAddress;
|
||||||
String customerEmail,
|
|
||||||
String customerType,
|
|
||||||
) {
|
|
||||||
return Card(
|
return Card(
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
elevation: 1,
|
elevation: 1,
|
||||||
@@ -626,46 +472,19 @@ class OrderDetailPage extends ConsumerWidget {
|
|||||||
|
|
||||||
const SizedBox(height: 16),
|
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),
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
_buildCustomerRow('Số điện thoại:', customerPhone),
|
_buildCustomerRow('Số điện thoại:', billingAddress.phone),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
_buildCustomerRow('Email:', customerEmail),
|
_buildCustomerRow('Email:', billingAddress.email),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
Row(
|
if (billingAddress.taxCode.isNotEmpty) ...[
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
_buildCustomerRow('Mã số thuế:', billingAddress.taxCode),
|
||||||
children: [
|
const SizedBox(height: 12),
|
||||||
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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -694,25 +513,13 @@ class OrderDetailPage extends ConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Build Products List Card
|
/// Build Products List Card
|
||||||
Widget _buildProductsListCard() {
|
Widget _buildProductsListCard(OrderDetail orderDetail) {
|
||||||
final products = [
|
final items = orderDetail.items;
|
||||||
{
|
final currencyFormatter = NumberFormat.currency(
|
||||||
'name': 'Gạch Eurotile MỘC LAM E03',
|
locale: 'vi_VN',
|
||||||
'size': '60x60cm',
|
symbol: 'đ',
|
||||||
'sku': 'ET-ML-E03-60x60',
|
decimalDigits: 0,
|
||||||
'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đ',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return Card(
|
return Card(
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
@@ -744,8 +551,8 @@ class OrderDetailPage extends ConsumerWidget {
|
|||||||
|
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
...products.map(
|
...items.map(
|
||||||
(product) => Container(
|
(item) => Container(
|
||||||
margin: const EdgeInsets.only(bottom: 12),
|
margin: const EdgeInsets.only(bottom: 12),
|
||||||
padding: const EdgeInsets.all(12),
|
padding: const EdgeInsets.all(12),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
@@ -756,6 +563,35 @@ class OrderDetailPage extends ConsumerWidget {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
// Product Image
|
// 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(
|
Container(
|
||||||
width: 60,
|
width: 60,
|
||||||
height: 60,
|
height: 60,
|
||||||
@@ -778,7 +614,7 @@ class OrderDetailPage extends ConsumerWidget {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
product['name']!,
|
item.itemName,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
@@ -787,14 +623,7 @@ class OrderDetailPage extends ConsumerWidget {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
Text(
|
Text(
|
||||||
'Kích thước: ${product['size']}',
|
'Mã: ${item.itemCode}',
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 12,
|
|
||||||
color: AppColors.grey500,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
'SKU: ${product['sku']}',
|
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: AppColors.grey500,
|
color: AppColors.grey500,
|
||||||
@@ -815,7 +644,7 @@ class OrderDetailPage extends ConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
product['quantity']!,
|
'${item.qtyOfSm} m²',
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
@@ -828,14 +657,14 @@ class OrderDetailPage extends ConsumerWidget {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
product['unitPrice']!,
|
'${currencyFormatter.format(item.price)}/m²',
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: AppColors.grey500,
|
color: AppColors.grey500,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
product['totalPrice']!,
|
currencyFormatter.format(item.totalAmount),
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
@@ -860,14 +689,9 @@ class OrderDetailPage extends ConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Build Order Summary Card
|
/// Build Order Summary Card
|
||||||
Widget _buildOrderSummaryCard(
|
Widget _buildOrderSummaryCard(OrderDetail orderDetail) {
|
||||||
double subtotal,
|
final order = orderDetail.order;
|
||||||
double shippingFee,
|
final paymentTerms = orderDetail.paymentTerms;
|
||||||
double discount,
|
|
||||||
double total,
|
|
||||||
String paymentMethod,
|
|
||||||
String? notes,
|
|
||||||
) {
|
|
||||||
final currencyFormatter = NumberFormat.currency(
|
final currencyFormatter = NumberFormat.currency(
|
||||||
locale: 'vi_VN',
|
locale: 'vi_VN',
|
||||||
symbol: 'đ',
|
symbol: 'đ',
|
||||||
@@ -904,35 +728,29 @@ class OrderDetailPage extends ConsumerWidget {
|
|||||||
|
|
||||||
const SizedBox(height: 16),
|
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),
|
const SizedBox(height: 8),
|
||||||
|
|
||||||
|
if (order.totalRemaining > 0) ...[
|
||||||
_buildSummaryRow(
|
_buildSummaryRow(
|
||||||
'Phí vận chuyển:',
|
'Còn lại:',
|
||||||
shippingFee == 0
|
currencyFormatter.format(order.totalRemaining),
|
||||||
? 'Miễn phí'
|
valueColor: AppColors.warning,
|
||||||
: currencyFormatter.format(shippingFee),
|
|
||||||
valueColor: shippingFee == 0 ? AppColors.success : null,
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
|
],
|
||||||
_buildSummaryRow(
|
|
||||||
'Giảm giá VIP:',
|
|
||||||
'-${currencyFormatter.format(discount)}',
|
|
||||||
valueColor: AppColors.success,
|
|
||||||
),
|
|
||||||
|
|
||||||
const Divider(height: 24),
|
const Divider(height: 24),
|
||||||
|
|
||||||
_buildSummaryRow(
|
_buildSummaryRow(
|
||||||
'Tổng cộng:',
|
'Tổng cộng:',
|
||||||
currencyFormatter.format(total),
|
currencyFormatter.format(order.grandTotal),
|
||||||
isTotal: true,
|
isTotal: true,
|
||||||
),
|
),
|
||||||
|
|
||||||
const Divider(height: 24),
|
const Divider(height: 24),
|
||||||
|
|
||||||
// Payment Method
|
// Payment Terms
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
const FaIcon(
|
const FaIcon(
|
||||||
@@ -942,22 +760,31 @@ class OrderDetailPage extends ConsumerWidget {
|
|||||||
),
|
),
|
||||||
const SizedBox(width: 6),
|
const SizedBox(width: 6),
|
||||||
const Text(
|
const Text(
|
||||||
'Phương thức thanh toán:',
|
'Điều khoản thanh toán:',
|
||||||
style: TextStyle(fontSize: 14, color: AppColors.grey500),
|
style: TextStyle(fontSize: 14, color: AppColors.grey500),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
Text(
|
Text(
|
||||||
paymentMethod,
|
paymentTerms.name,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w600,
|
||||||
color: AppColors.grey900,
|
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),
|
const Divider(height: 24),
|
||||||
|
|
||||||
// Order Notes
|
// Order Notes
|
||||||
@@ -973,7 +800,7 @@ class OrderDetailPage extends ConsumerWidget {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
Text(
|
Text(
|
||||||
notes,
|
order.description,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
@@ -1019,56 +846,73 @@ class OrderDetailPage extends ConsumerWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get mock order data for development
|
/// Build Action Buttons
|
||||||
Map<String, dynamic> _getMockOrder() {
|
Widget _buildActionButtons(BuildContext context, OrderDetail orderDetail) {
|
||||||
return {
|
final shippingAddress = orderDetail.shippingAddress;
|
||||||
'orderNumber': 'DH001234',
|
|
||||||
'status': OrderStatus.processing,
|
return Container(
|
||||||
'statusHistory': [
|
decoration: BoxDecoration(
|
||||||
{
|
color: AppColors.white,
|
||||||
'title': 'Đơn hàng được tạo',
|
boxShadow: [
|
||||||
'date': '03/08/2023 - 09:30',
|
BoxShadow(
|
||||||
'status': 'completed',
|
color: Colors.black.withValues(alpha: 0.1),
|
||||||
},
|
blurRadius: 15,
|
||||||
{
|
offset: const Offset(0, -4),
|
||||||
'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),
|
padding: const EdgeInsets.all(16),
|
||||||
'deliveryDate': DateTime(2023, 8, 7),
|
child: Row(
|
||||||
'deliveryAddress':
|
spacing: 12,
|
||||||
'123 Đường Lê Văn Lương, Phường Tân Hưng,\nQuận 7, TP. Hồ Chí Minh',
|
children: [
|
||||||
'receiverName': 'Nguyễn Văn A',
|
Expanded(
|
||||||
'receiverPhone': '0901234567',
|
child: OutlinedButton.icon(
|
||||||
'customerName': 'Nguyễn Văn A',
|
onPressed: () {
|
||||||
'customerPhone': '0901234567',
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
'customerEmail': 'nguyenvana@email.com',
|
SnackBar(
|
||||||
'customerType': 'Khách VIP',
|
content: Text('Gọi ${shippingAddress.phone}...'),
|
||||||
'subtotal': 12900000.0,
|
),
|
||||||
'shippingFee': 0.0,
|
);
|
||||||
'discount': 129000.0,
|
},
|
||||||
'total': 12771000.0,
|
icon: const FaIcon(FontAwesomeIcons.phone, size: 18),
|
||||||
'paymentMethod': 'Chuyển khoản ngân hàng',
|
label: const Text('Liên hệ'),
|
||||||
'notes':
|
style: OutlinedButton.styleFrom(
|
||||||
'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.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:riverpod_annotation/riverpod_annotation.dart';
|
||||||
import 'package:worker/features/orders/domain/entities/order.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/order_status.dart';
|
||||||
import 'package:worker/features/orders/presentation/providers/order_repository_provider.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);
|
final repository = await ref.watch(orderRepositoryProvider.future);
|
||||||
return await repository.getOrderStatusList();
|
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';
|
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