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