list orders

This commit is contained in:
Phuoc Nguyen
2025-11-24 16:25:54 +07:00
parent 354df3ad01
commit 75d6507719
24 changed files with 1004 additions and 982 deletions

View File

@@ -247,4 +247,76 @@ class OrderRemoteDataSource {
throw Exception('Failed to upload bill: $e');
}
}
/// Get list of orders
///
/// Calls: POST /api/method/building_material.building_material.api.sales_order.get_list
/// Body: { "limit_start": 0, "limit_page_length": 0 }
/// Returns: List of orders
Future<List<Map<String, dynamic>>> getOrdersList({
int limitStart = 0,
int limitPageLength = 0,
}) async {
try {
final response = await _dioClient.post<Map<String, dynamic>>(
'${ApiConstants.frappeApiMethod}${ApiConstants.getOrdersList}',
data: {
'limit_start': limitStart,
'limit_page_length': limitPageLength,
},
);
final data = response.data;
if (data == null) {
throw Exception('No data received from getOrdersList API');
}
// Extract orders list from Frappe response
final message = data['message'];
if (message == null) {
throw Exception('No message field in getOrdersList response');
}
if (message is! List) {
throw Exception('Expected list but got ${message.runtimeType}');
}
return message.cast<Map<String, dynamic>>();
} catch (e) {
throw Exception('Failed to get orders list: $e');
}
}
/// Get order detail
///
/// Calls: POST /api/method/building_material.building_material.api.sales_order.get_detail
/// Body: { "name": "SAL-ORD-2025-00058-1" }
/// Returns: Order details
Future<Map<String, dynamic>> getOrderDetail(String orderName) async {
try {
final response = await _dioClient.post<Map<String, dynamic>>(
'${ApiConstants.frappeApiMethod}${ApiConstants.getOrderDetail}',
data: {'name': orderName},
);
final data = response.data;
if (data == null) {
throw Exception('No data received from getOrderDetail API');
}
// Extract order detail from Frappe response
final message = data['message'];
if (message == null) {
throw Exception('No message field in getOrderDetail response');
}
if (message is! Map<String, dynamic>) {
throw Exception('Expected map but got ${message.runtimeType}');
}
return message;
} catch (e) {
throw Exception('Failed to get order detail: $e');
}
}
}

View File

@@ -0,0 +1,47 @@
/// Order Status Local Data Source
///
/// Handles local caching of order status list using Hive.
library;
import 'package:hive_ce/hive.dart';
import 'package:worker/core/constants/storage_constants.dart';
import 'package:worker/features/orders/data/models/order_status_model.dart';
/// Order Status Local Data Source
class OrderStatusLocalDataSource {
/// Get Hive box for order statuses
Box<dynamic> get _box => Hive.box(HiveBoxNames.orderStatusBox);
/// Save order status list to cache
Future<void> cacheStatusList(List<OrderStatusModel> statuses) async {
// Clear existing cache
await _box.clear();
// Save each status with its index as key
for (final status in statuses) {
await _box.put(status.index, status);
}
}
/// Get cached order status list
List<OrderStatusModel> getCachedStatusList() {
try {
final values = _box.values.whereType<OrderStatusModel>().toList();
// Sort by index
values.sort((a, b) => a.index.compareTo(b.index));
return values;
} catch (e) {
return [];
}
}
/// Check if cache exists and is not empty
bool hasCachedData() {
return _box.isNotEmpty;
}
/// Clear all cached statuses
Future<void> clearCache() async {
await _box.clear();
}
}

View File

@@ -1,215 +0,0 @@
/// Local Data Source: Orders
///
/// Provides mock order data for development and testing.
library;
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:worker/core/database/models/enums.dart';
import 'package:worker/features/orders/data/models/order_model.dart';
/// Orders Local Data Source
///
/// Manages local mock order data.
class OrdersLocalDataSource {
/// Get all mock orders
Future<List<OrderModel>> getAllOrders() async {
try {
debugPrint('[OrdersLocalDataSource] Loading mock orders...');
// Parse mock JSON data
final decoded = jsonDecode(_mockOrdersJson);
if (decoded is! List) {
throw Exception('Invalid JSON format: expected List');
}
final orders = decoded
.map((json) => OrderModel.fromJson(json as Map<String, dynamic>))
.toList();
debugPrint('[OrdersLocalDataSource] Loaded ${orders.length} orders');
return orders;
} catch (e, stackTrace) {
debugPrint('[OrdersLocalDataSource] Error loading orders: $e');
debugPrint('Stack trace: $stackTrace');
rethrow;
}
}
/// Get orders by status
Future<List<OrderModel>> getOrdersByStatus(OrderStatus status) async {
try {
final allOrders = await getAllOrders();
final filtered = allOrders
.where((order) => order.status == status)
.toList();
debugPrint(
'[OrdersLocalDataSource] Filtered ${filtered.length} orders with status: $status',
);
return filtered;
} catch (e) {
debugPrint('[OrdersLocalDataSource] Error filtering orders: $e');
rethrow;
}
}
/// Search orders by order number
Future<List<OrderModel>> searchOrders(String query) async {
try {
if (query.isEmpty) {
return getAllOrders();
}
final allOrders = await getAllOrders();
final filtered = allOrders
.where(
(order) =>
order.orderNumber.toLowerCase().contains(query.toLowerCase()),
)
.toList();
debugPrint(
'[OrdersLocalDataSource] Found ${filtered.length} orders matching "$query"',
);
return filtered;
} catch (e) {
debugPrint('[OrdersLocalDataSource] Error searching orders: $e');
rethrow;
}
}
/// Get order by ID
Future<OrderModel?> getOrderById(String orderId) async {
try {
final allOrders = await getAllOrders();
final order = allOrders.firstWhere(
(order) => order.orderId == orderId,
orElse: () => throw Exception('Order not found: $orderId'),
);
debugPrint('[OrdersLocalDataSource] Found order: ${order.orderNumber}');
return order;
} catch (e) {
debugPrint('[OrdersLocalDataSource] Error getting order: $e');
return null;
}
}
/// Mock orders JSON data
/// Matches the HTML design with 5 sample orders
static const String _mockOrdersJson = '''
[
{
"order_id": "ord_001",
"order_number": "DH001234",
"user_id": "user_001",
"status": "processing",
"total_amount": 12900000,
"discount_amount": 0,
"tax_amount": 0,
"shipping_fee": 0,
"final_amount": 12900000,
"shipping_address": {
"name": "Nguyễn Văn A",
"phone": "0901234567",
"street": "123 Đường Nguyễn Văn Linh",
"district": "Quận 7",
"city": "HCM",
"postal_code": "70000"
},
"expected_delivery_date": "2025-08-06T00:00:00.000Z",
"created_at": "2025-08-03T00:00:00.000Z",
"updated_at": "2025-08-03T00:00:00.000Z"
},
{
"order_id": "ord_002",
"order_number": "DH001233",
"user_id": "user_001",
"status": "completed",
"total_amount": 8500000,
"discount_amount": 0,
"tax_amount": 0,
"shipping_fee": 0,
"final_amount": 8500000,
"shipping_address": {
"name": "Trần Thị B",
"phone": "0912345678",
"street": "456 Đại lộ Bình Dương",
"city": "Thủ Dầu Một, Bình Dương",
"postal_code": "75000"
},
"expected_delivery_date": "2025-06-27T00:00:00.000Z",
"actual_delivery_date": "2025-06-27T00:00:00.000Z",
"created_at": "2025-06-24T00:00:00.000Z",
"updated_at": "2025-06-27T00:00:00.000Z"
},
{
"order_id": "ord_003",
"order_number": "DH001232",
"user_id": "user_001",
"status": "shipped",
"total_amount": 15200000,
"discount_amount": 0,
"tax_amount": 0,
"shipping_fee": 0,
"final_amount": 15200000,
"shipping_address": {
"name": "Lê Văn C",
"phone": "0923456789",
"street": "789 Phố Duy Tân",
"district": "Cầu Giấy",
"city": "Hà Nội",
"postal_code": "10000"
},
"expected_delivery_date": "2025-03-05T00:00:00.000Z",
"created_at": "2025-03-01T00:00:00.000Z",
"updated_at": "2025-03-02T00:00:00.000Z"
},
{
"order_id": "ord_004",
"order_number": "DH001231",
"user_id": "user_001",
"status": "pending",
"total_amount": 6750000,
"discount_amount": 0,
"tax_amount": 0,
"shipping_fee": 0,
"final_amount": 6750000,
"shipping_address": {
"name": "Phạm Thị D",
"phone": "0934567890",
"street": "321 Đường Võ Văn Ngân",
"city": "Thủ Đức, HCM",
"postal_code": "71000"
},
"expected_delivery_date": "2024-11-12T00:00:00.000Z",
"created_at": "2024-11-08T00:00:00.000Z",
"updated_at": "2024-11-08T00:00:00.000Z"
},
{
"order_id": "ord_005",
"order_number": "DH001230",
"user_id": "user_001",
"status": "cancelled",
"total_amount": 3200000,
"discount_amount": 0,
"tax_amount": 0,
"shipping_fee": 0,
"final_amount": 3200000,
"shipping_address": {
"name": "Hoàng Văn E",
"phone": "0945678901",
"street": "654 Đường 3 Tháng 2",
"city": "Rạch Giá, Kiên Giang",
"postal_code": "92000"
},
"expected_delivery_date": "2024-08-04T00:00:00.000Z",
"cancellation_reason": "Khách hàng yêu cầu hủy",
"created_at": "2024-07-30T00:00:00.000Z",
"updated_at": "2024-07-31T00:00:00.000Z"
}
]
''';
}

View File

@@ -1,161 +1,117 @@
import 'dart:convert';
import 'package:hive_ce/hive.dart';
import 'package:worker/core/constants/storage_constants.dart';
import 'package:worker/core/database/models/enums.dart';
import 'package:worker/features/orders/domain/entities/order.dart';
part 'order_model.g.dart';
/// Order Model - Type ID: 6
///
/// Simplified model matching API response structure
@HiveType(typeId: HiveTypeIds.orderModel)
class OrderModel extends HiveObject {
/// Order ID/Number (from API 'name' field)
@HiveField(0)
final String name;
/// Transaction date
@HiveField(1)
final String transactionDate;
/// Expected delivery date
@HiveField(2)
final String deliveryDate;
/// Delivery address
@HiveField(3)
final String address;
/// Grand total amount
@HiveField(4)
final double grandTotal;
/// Status label (Vietnamese)
@HiveField(5)
final String status;
/// Status color (Warning, Success, Danger, Info, Secondary)
@HiveField(6)
final String statusColor;
OrderModel({
required this.orderId,
required this.orderNumber,
required this.userId,
required this.name,
required this.transactionDate,
required this.deliveryDate,
required this.address,
required this.grandTotal,
required this.status,
required this.totalAmount,
required this.discountAmount,
required this.taxAmount,
required this.shippingFee,
required this.finalAmount,
this.shippingAddress,
this.billingAddress,
this.expectedDeliveryDate,
this.actualDeliveryDate,
this.notes,
this.cancellationReason,
this.erpnextSalesOrder,
required this.createdAt,
this.updatedAt,
required this.statusColor,
});
@HiveField(0)
final String orderId;
@HiveField(1)
final String orderNumber;
@HiveField(2)
final String userId;
@HiveField(3)
final OrderStatus status;
@HiveField(4)
final double totalAmount;
@HiveField(5)
final double discountAmount;
@HiveField(6)
final double taxAmount;
@HiveField(7)
final double shippingFee;
@HiveField(8)
final double finalAmount;
@HiveField(9)
final String? shippingAddress;
@HiveField(10)
final String? billingAddress;
@HiveField(11)
final DateTime? expectedDeliveryDate;
@HiveField(12)
final DateTime? actualDeliveryDate;
@HiveField(13)
final String? notes;
@HiveField(14)
final String? cancellationReason;
@HiveField(15)
final String? erpnextSalesOrder;
@HiveField(16)
final DateTime createdAt;
@HiveField(17)
final DateTime? updatedAt;
/// Create from JSON (API response)
factory OrderModel.fromJson(Map<String, dynamic> json) {
return OrderModel(
orderId: json['order_id'] as String,
orderNumber: json['order_number'] as String,
userId: json['user_id'] as String,
status: OrderStatus.values.firstWhere((e) => e.name == json['status']),
totalAmount: (json['total_amount'] as num).toDouble(),
discountAmount: (json['discount_amount'] as num).toDouble(),
taxAmount: (json['tax_amount'] as num).toDouble(),
shippingFee: (json['shipping_fee'] as num).toDouble(),
finalAmount: (json['final_amount'] as num).toDouble(),
shippingAddress: json['shipping_address'] != null
? jsonEncode(json['shipping_address'])
: null,
billingAddress: json['billing_address'] != null
? jsonEncode(json['billing_address'])
: null,
expectedDeliveryDate: json['expected_delivery_date'] != null
? DateTime.parse(json['expected_delivery_date']?.toString() ?? '')
: null,
actualDeliveryDate: json['actual_delivery_date'] != null
? DateTime.parse(json['actual_delivery_date']?.toString() ?? '')
: null,
notes: json['notes'] as String?,
cancellationReason: json['cancellation_reason'] as String?,
erpnextSalesOrder: json['erpnext_sales_order'] as String?,
createdAt: DateTime.parse(json['created_at']?.toString() ?? ''),
updatedAt: json['updated_at'] != null
? DateTime.parse(json['updated_at']?.toString() ?? '')
: null,
name: json['name'] as String? ?? '',
transactionDate: json['transaction_date'] as String? ?? '',
deliveryDate: json['delivery_date'] as String? ?? '',
address: json['address'] as String? ?? '',
grandTotal: (json['grand_total'] as num?)?.toDouble() ?? 0.0,
status: json['status'] as String? ?? '',
statusColor: json['status_color'] as String? ?? 'Secondary',
);
}
/// Convert to JSON
Map<String, dynamic> toJson() => {
'order_id': orderId,
'order_number': orderNumber,
'user_id': userId,
'status': status.name,
'total_amount': totalAmount,
'discount_amount': discountAmount,
'tax_amount': taxAmount,
'shipping_fee': shippingFee,
'final_amount': finalAmount,
'shipping_address': shippingAddress != null
? jsonDecode(shippingAddress!)
: null,
'billing_address': billingAddress != null
? jsonDecode(billingAddress!)
: null,
'expected_delivery_date': expectedDeliveryDate?.toIso8601String(),
'actual_delivery_date': actualDeliveryDate?.toIso8601String(),
'notes': notes,
'cancellation_reason': cancellationReason,
'erpnext_sales_order': erpnextSalesOrder,
'created_at': createdAt.toIso8601String(),
'updated_at': updatedAt?.toIso8601String(),
};
'name': name,
'transaction_date': transactionDate,
'delivery_date': deliveryDate,
'address': address,
'grand_total': grandTotal,
'status': status,
'status_color': statusColor,
};
Map<String, dynamic>? get shippingAddressMap {
if (shippingAddress == null) return null;
/// Get parsed transaction date
DateTime? get transactionDateTime {
try {
return jsonDecode(shippingAddress!) as Map<String, dynamic>;
return DateTime.parse(transactionDate);
} catch (e) {
return null;
}
}
Map<String, dynamic>? get billingAddressMap {
if (billingAddress == null) return null;
/// Get parsed delivery date
DateTime? get deliveryDateTime {
try {
return jsonDecode(billingAddress!) as Map<String, dynamic>;
return DateTime.parse(deliveryDate);
} catch (e) {
return null;
}
}
/// Convert to domain entity
Order toEntity() {
return Order(
name: name,
transactionDate: transactionDate,
deliveryDate: deliveryDate,
address: address,
grandTotal: grandTotal,
status: status,
statusColor: statusColor,
);
}
/// Create from domain entity
factory OrderModel.fromEntity(Order entity) {
return OrderModel(
name: entity.name,
transactionDate: entity.transactionDate,
deliveryDate: entity.deliveryDate,
address: entity.address,
grandTotal: entity.grandTotal,
status: entity.status,
statusColor: entity.statusColor,
);
}
}

View File

@@ -17,67 +17,34 @@ class OrderModelAdapter extends TypeAdapter<OrderModel> {
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return OrderModel(
orderId: fields[0] as String,
orderNumber: fields[1] as String,
userId: fields[2] as String,
status: fields[3] as OrderStatus,
totalAmount: (fields[4] as num).toDouble(),
discountAmount: (fields[5] as num).toDouble(),
taxAmount: (fields[6] as num).toDouble(),
shippingFee: (fields[7] as num).toDouble(),
finalAmount: (fields[8] as num).toDouble(),
shippingAddress: fields[9] as String?,
billingAddress: fields[10] as String?,
expectedDeliveryDate: fields[11] as DateTime?,
actualDeliveryDate: fields[12] as DateTime?,
notes: fields[13] as String?,
cancellationReason: fields[14] as String?,
erpnextSalesOrder: fields[15] as String?,
createdAt: fields[16] as DateTime,
updatedAt: fields[17] as DateTime?,
name: fields[0] as String,
transactionDate: fields[1] as String,
deliveryDate: fields[2] as String,
address: fields[3] as String,
grandTotal: (fields[4] as num).toDouble(),
status: fields[5] as String,
statusColor: fields[6] as String,
);
}
@override
void write(BinaryWriter writer, OrderModel obj) {
writer
..writeByte(18)
..writeByte(0)
..write(obj.orderId)
..writeByte(1)
..write(obj.orderNumber)
..writeByte(2)
..write(obj.userId)
..writeByte(3)
..write(obj.status)
..writeByte(4)
..write(obj.totalAmount)
..writeByte(5)
..write(obj.discountAmount)
..writeByte(6)
..write(obj.taxAmount)
..writeByte(7)
..write(obj.shippingFee)
..writeByte(8)
..write(obj.finalAmount)
..writeByte(9)
..write(obj.shippingAddress)
..writeByte(10)
..write(obj.billingAddress)
..writeByte(11)
..write(obj.expectedDeliveryDate)
..writeByte(12)
..write(obj.actualDeliveryDate)
..writeByte(13)
..write(obj.notes)
..writeByte(14)
..write(obj.cancellationReason)
..writeByte(15)
..write(obj.erpnextSalesOrder)
..writeByte(16)
..write(obj.createdAt)
..writeByte(17)
..write(obj.updatedAt);
..writeByte(0)
..write(obj.name)
..writeByte(1)
..write(obj.transactionDate)
..writeByte(2)
..write(obj.deliveryDate)
..writeByte(3)
..write(obj.address)
..writeByte(4)
..write(obj.grandTotal)
..writeByte(5)
..write(obj.status)
..writeByte(6)
..write(obj.statusColor);
}
@override

View File

@@ -1,19 +1,30 @@
/// Order Status Model
///
/// Data model for order status from API responses.
/// Data model for order status from API responses with Hive caching.
library;
import 'package:equatable/equatable.dart';
import 'package:hive_ce/hive.dart';
import 'package:worker/core/constants/storage_constants.dart';
import 'package:worker/features/orders/domain/entities/order_status.dart';
/// Order Status Model
class OrderStatusModel extends Equatable {
part 'order_status_model.g.dart';
/// Order Status Model - Type ID: 62
@HiveType(typeId: HiveTypeIds.orderStatusModel)
class OrderStatusModel extends HiveObject {
@HiveField(0)
final String status;
@HiveField(1)
final String label;
@HiveField(2)
final String color;
@HiveField(3)
final int index;
const OrderStatusModel({
OrderStatusModel({
required this.status,
required this.label,
required this.color,
@@ -59,7 +70,4 @@ class OrderStatusModel extends Equatable {
index: entity.index,
);
}
@override
List<Object?> get props => [status, label, color, index];
}

View File

@@ -0,0 +1,50 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'order_status_model.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class OrderStatusModelAdapter extends TypeAdapter<OrderStatusModel> {
@override
final typeId = 62;
@override
OrderStatusModel read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return OrderStatusModel(
status: fields[0] as String,
label: fields[1] as String,
color: fields[2] as String,
index: (fields[3] as num).toInt(),
);
}
@override
void write(BinaryWriter writer, OrderStatusModel obj) {
writer
..writeByte(4)
..writeByte(0)
..write(obj.status)
..writeByte(1)
..write(obj.label)
..writeByte(2)
..write(obj.color)
..writeByte(3)
..write(obj.index);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is OrderStatusModelAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}

View File

@@ -4,22 +4,67 @@
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_model.dart';
import 'package:worker/features/orders/domain/entities/order.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';
/// Order Repository Implementation
class OrderRepositoryImpl implements OrderRepository {
const OrderRepositoryImpl(this._remoteDataSource);
const OrderRepositoryImpl(
this._remoteDataSource,
this._statusLocalDataSource,
);
final OrderRemoteDataSource _remoteDataSource;
final OrderStatusLocalDataSource _statusLocalDataSource;
@override
Future<List<Order>> getOrdersList({
int limitStart = 0,
int limitPageLength = 0,
}) async {
try {
final ordersData = await _remoteDataSource.getOrdersList(
limitStart: limitStart,
limitPageLength: limitPageLength,
);
// Convert JSON → Model → Entity
return ordersData
.map((json) => OrderModel.fromJson(json).toEntity())
.toList();
} catch (e) {
throw Exception('Failed to get orders list: $e');
}
}
@override
Future<List<OrderStatus>> getOrderStatusList() async {
try {
// Try to get from cache first
if (_statusLocalDataSource.hasCachedData()) {
final cachedModels = _statusLocalDataSource.getCachedStatusList();
if (cachedModels.isNotEmpty) {
return cachedModels.map((model) => model.toEntity()).toList();
}
}
// Fetch from API
final models = await _remoteDataSource.getOrderStatusList();
// Cache the results
await _statusLocalDataSource.cacheStatusList(models);
// Return entities
return models.map((model) => model.toEntity()).toList();
} catch (e) {
// If API fails, try to return cached data
final cachedModels = _statusLocalDataSource.getCachedStatusList();
if (cachedModels.isNotEmpty) {
return cachedModels.map((model) => model.toEntity()).toList();
}
throw Exception('Failed to get order status list: $e');
}
}

View File

@@ -1,321 +1,97 @@
/// Domain Entity: Order
///
/// Represents a customer order.
/// Represents a customer order (simplified to match API structure).
library;
/// Order status enum
enum OrderStatus {
/// Order has been created but not confirmed
draft,
/// Order has been confirmed
confirmed,
/// Order is being processed
processing,
/// Order is ready for shipping
ready,
/// Order has been shipped
shipped,
/// Order has been delivered
delivered,
/// Order has been completed
completed,
/// Order has been cancelled
cancelled,
/// Order has been returned
returned;
/// Get display name for status
String get displayName {
switch (this) {
case OrderStatus.draft:
return 'Draft';
case OrderStatus.confirmed:
return 'Confirmed';
case OrderStatus.processing:
return 'Processing';
case OrderStatus.ready:
return 'Ready';
case OrderStatus.shipped:
return 'Shipped';
case OrderStatus.delivered:
return 'Delivered';
case OrderStatus.completed:
return 'Completed';
case OrderStatus.cancelled:
return 'Cancelled';
case OrderStatus.returned:
return 'Returned';
}
}
}
/// Address information
class Address {
/// Recipient name
final String? name;
/// Phone number
final String? phone;
/// Street address
final String? street;
/// Ward/commune
final String? ward;
/// District
final String? district;
/// City/province
final String? city;
/// Postal code
final String? postalCode;
const Address({
this.name,
this.phone,
this.street,
this.ward,
this.district,
this.city,
this.postalCode,
});
/// Get full address string
String get fullAddress {
final parts = [
street,
ward,
district,
city,
postalCode,
].where((part) => part != null && part.isNotEmpty).toList();
return parts.join(', ');
}
/// Create from JSON map
factory Address.fromJson(Map<String, dynamic> json) {
return Address(
name: json['name'] as String?,
phone: json['phone'] as String?,
street: json['street'] as String?,
ward: json['ward'] as String?,
district: json['district'] as String?,
city: json['city'] as String?,
postalCode: json['postal_code'] as String?,
);
}
/// Convert to JSON map
Map<String, dynamic> toJson() {
return {
'name': name,
'phone': phone,
'street': street,
'ward': ward,
'district': district,
'city': city,
'postal_code': postalCode,
};
}
}
import 'package:equatable/equatable.dart';
/// Order Entity
///
/// Contains complete order information:
/// - Order identification
/// - Customer details
/// - Pricing and discounts
/// - Shipping information
/// - Status tracking
class Order {
/// Unique order identifier
final String orderId;
/// Pure domain entity matching API response structure
class Order extends Equatable {
/// Order ID/Number
final String name;
/// Human-readable order number
final String orderNumber;
/// Transaction date (ISO format string)
final String transactionDate;
/// User ID who placed the order
final String userId;
/// Expected delivery date (ISO format string)
final String deliveryDate;
/// Current order status
final OrderStatus status;
/// Delivery address
final String address;
/// Total order amount before discounts
final double totalAmount;
/// Grand total amount
final double grandTotal;
/// Discount amount applied
final double discountAmount;
/// Status label (Vietnamese)
final String status;
/// Tax amount
final double taxAmount;
/// Shipping fee
final double shippingFee;
/// Final amount to pay
final double finalAmount;
/// Shipping address
final Address? shippingAddress;
/// Billing address
final Address? billingAddress;
/// Expected delivery date
final DateTime? expectedDeliveryDate;
/// Actual delivery date
final DateTime? actualDeliveryDate;
/// Order notes
final String? notes;
/// Cancellation reason
final String? cancellationReason;
/// ERPNext sales order reference
final String? erpnextSalesOrder;
/// Order creation timestamp
final DateTime createdAt;
/// Last update timestamp
final DateTime updatedAt;
/// Status color (Warning, Success, Danger, Info, Secondary)
final String statusColor;
const Order({
required this.orderId,
required this.orderNumber,
required this.userId,
required this.name,
required this.transactionDate,
required this.deliveryDate,
required this.address,
required this.grandTotal,
required this.status,
required this.totalAmount,
required this.discountAmount,
required this.taxAmount,
required this.shippingFee,
required this.finalAmount,
this.shippingAddress,
this.billingAddress,
this.expectedDeliveryDate,
this.actualDeliveryDate,
this.notes,
this.cancellationReason,
this.erpnextSalesOrder,
required this.createdAt,
required this.updatedAt,
required this.statusColor,
});
/// Check if order is active (not cancelled or completed)
bool get isActive =>
status != OrderStatus.cancelled &&
status != OrderStatus.completed &&
status != OrderStatus.returned;
/// Get parsed transaction date
DateTime? get transactionDateTime {
try {
return DateTime.parse(transactionDate);
} catch (e) {
return null;
}
}
/// Check if order can be cancelled
bool get canBeCancelled =>
status == OrderStatus.draft ||
status == OrderStatus.confirmed ||
status == OrderStatus.processing;
/// Check if order is delivered
bool get isDelivered =>
status == OrderStatus.delivered || status == OrderStatus.completed;
/// Check if order is cancelled
bool get isCancelled => status == OrderStatus.cancelled;
/// Get discount percentage
double get discountPercentage {
if (totalAmount == 0) return 0;
return (discountAmount / totalAmount) * 100;
/// Get parsed delivery date
DateTime? get deliveryDateTime {
try {
return DateTime.parse(deliveryDate);
} catch (e) {
return null;
}
}
/// Copy with method for immutability
Order copyWith({
String? orderId,
String? orderNumber,
String? userId,
OrderStatus? status,
double? totalAmount,
double? discountAmount,
double? taxAmount,
double? shippingFee,
double? finalAmount,
Address? shippingAddress,
Address? billingAddress,
DateTime? expectedDeliveryDate,
DateTime? actualDeliveryDate,
String? notes,
String? cancellationReason,
String? erpnextSalesOrder,
DateTime? createdAt,
DateTime? updatedAt,
String? name,
String? transactionDate,
String? deliveryDate,
String? address,
double? grandTotal,
String? status,
String? statusColor,
}) {
return Order(
orderId: orderId ?? this.orderId,
orderNumber: orderNumber ?? this.orderNumber,
userId: userId ?? this.userId,
name: name ?? this.name,
transactionDate: transactionDate ?? this.transactionDate,
deliveryDate: deliveryDate ?? this.deliveryDate,
address: address ?? this.address,
grandTotal: grandTotal ?? this.grandTotal,
status: status ?? this.status,
totalAmount: totalAmount ?? this.totalAmount,
discountAmount: discountAmount ?? this.discountAmount,
taxAmount: taxAmount ?? this.taxAmount,
shippingFee: shippingFee ?? this.shippingFee,
finalAmount: finalAmount ?? this.finalAmount,
shippingAddress: shippingAddress ?? this.shippingAddress,
billingAddress: billingAddress ?? this.billingAddress,
expectedDeliveryDate: expectedDeliveryDate ?? this.expectedDeliveryDate,
actualDeliveryDate: actualDeliveryDate ?? this.actualDeliveryDate,
notes: notes ?? this.notes,
cancellationReason: cancellationReason ?? this.cancellationReason,
erpnextSalesOrder: erpnextSalesOrder ?? this.erpnextSalesOrder,
createdAt: createdAt ?? this.createdAt,
updatedAt: updatedAt ?? this.updatedAt,
statusColor: statusColor ?? this.statusColor,
);
}
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is Order &&
other.orderId == orderId &&
other.orderNumber == orderNumber &&
other.userId == userId &&
other.status == status &&
other.totalAmount == totalAmount &&
other.discountAmount == discountAmount &&
other.taxAmount == taxAmount &&
other.shippingFee == shippingFee &&
other.finalAmount == finalAmount;
}
@override
int get hashCode {
return Object.hash(
orderId,
orderNumber,
userId,
status,
totalAmount,
discountAmount,
taxAmount,
shippingFee,
finalAmount,
);
}
List<Object?> get props => [
name,
transactionDate,
deliveryDate,
address,
grandTotal,
status,
statusColor,
];
@override
String toString() {
return 'Order(orderId: $orderId, orderNumber: $orderNumber, status: $status, '
'finalAmount: $finalAmount, createdAt: $createdAt)';
return 'Order(name: $name, status: $status, grandTotal: $grandTotal, transactionDate: $transactionDate)';
}
}

View File

@@ -3,11 +3,18 @@
/// Defines the contract for order-related data operations.
library;
import 'package:worker/features/orders/domain/entities/order.dart';
import 'package:worker/features/orders/domain/entities/order_status.dart';
import 'package:worker/features/orders/domain/entities/payment_term.dart';
/// Order Repository Interface
abstract class OrderRepository {
/// Get list of orders
Future<List<Order>> getOrdersList({
int limitStart = 0,
int limitPageLength = 0,
});
/// Get list of available order statuses
Future<List<OrderStatus>> getOrderStatusList();

View File

@@ -8,7 +8,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:go_router/go_router.dart';
import 'package:worker/core/constants/ui_constants.dart';
import 'package:worker/core/database/models/enums.dart';
import 'package:worker/core/theme/colors.dart';
import 'package:worker/features/orders/presentation/providers/orders_provider.dart';
import 'package:worker/features/orders/presentation/widgets/order_card.dart';
@@ -77,16 +76,28 @@ class _OrdersPageState extends ConsumerState<OrdersPage> {
},
child: CustomScrollView(
slivers: [
// Search Bar
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.all(16),
child: _buildSearchBar(),
// Sticky Search Bar
SliverPersistentHeader(
pinned: true,
delegate: _SearchBarDelegate(
child: Container(
color: const Color(0xFFF4F6F8),
padding: const EdgeInsets.all(16),
child: _buildSearchBar(),
),
),
),
// Filter Pills
SliverToBoxAdapter(child: _buildFilterPills(selectedStatus)),
// Sticky Filter Pills
SliverPersistentHeader(
pinned: true,
delegate: _FilterPillsDelegate(
child: Container(
color: const Color(0xFFF4F6F8),
child: _buildFilterPills(selectedStatus),
),
),
),
// Orders List
SliverPadding(
@@ -103,7 +114,7 @@ class _OrdersPageState extends ConsumerState<OrdersPage> {
return OrderCard(
order: order,
onTap: () {
context.push('/orders/${order.orderId}');
context.push('/orders/${order.name}');
},
);
}, childCount: orders.length),
@@ -168,83 +179,74 @@ class _OrdersPageState extends ConsumerState<OrdersPage> {
);
}
/// Build filter pills
Widget _buildFilterPills(OrderStatus? selectedStatus) {
return Container(
/// Build filter pills (dynamically from cached status list)
Widget _buildFilterPills(String? selectedStatus) {
final statusListAsync = ref.watch(orderStatusListProvider);
return SizedBox(
height: 48,
padding: const EdgeInsets.symmetric(horizontal: 16),
child: ListView(
scrollDirection: Axis.horizontal,
children: [
// All filter
_buildFilterChip(
label: 'Tất cả',
isSelected: selectedStatus == null,
onTap: () {
ref.read(selectedOrderStatusProvider.notifier).clearSelection();
},
),
const SizedBox(width: 8),
child: statusListAsync.when(
data: (statusList) {
return ListView(
padding: const EdgeInsets.symmetric(horizontal: 16),
scrollDirection: Axis.horizontal,
children: [
// All filter (always first)
_buildFilterChip(
label: 'Tất cả',
isSelected: selectedStatus == null,
onTap: () {
ref.read(selectedOrderStatusProvider.notifier).clearSelection();
},
),
const SizedBox(width: 8),
// Pending filter
_buildFilterChip(
label: 'Chờ xác nhận',
isSelected: selectedStatus == OrderStatus.pending,
onTap: () {
ref
.read(selectedOrderStatusProvider.notifier)
.selectStatus(OrderStatus.pending);
},
),
const SizedBox(width: 8),
// Processing filter
_buildFilterChip(
label: 'Đang xử lý',
isSelected: selectedStatus == OrderStatus.processing,
onTap: () {
ref
.read(selectedOrderStatusProvider.notifier)
.selectStatus(OrderStatus.processing);
},
),
const SizedBox(width: 8),
// Shipped filter
_buildFilterChip(
label: 'Đang giao',
isSelected: selectedStatus == OrderStatus.shipped,
onTap: () {
ref
.read(selectedOrderStatusProvider.notifier)
.selectStatus(OrderStatus.shipped);
},
),
const SizedBox(width: 8),
// Completed filter
_buildFilterChip(
label: 'Hoàn thành',
isSelected: selectedStatus == OrderStatus.completed,
onTap: () {
ref
.read(selectedOrderStatusProvider.notifier)
.selectStatus(OrderStatus.completed);
},
),
const SizedBox(width: 8),
// Cancelled filter
_buildFilterChip(
label: 'Đã hủy',
isSelected: selectedStatus == OrderStatus.cancelled,
onTap: () {
ref
.read(selectedOrderStatusProvider.notifier)
.selectStatus(OrderStatus.cancelled);
},
),
],
// Dynamic status filters from API
...statusList.map((status) {
return Padding(
padding: const EdgeInsets.only(right: 8),
child: _buildFilterChip(
label: status.label,
isSelected: selectedStatus == status.label,
onTap: () {
ref
.read(selectedOrderStatusProvider.notifier)
.selectStatus(status.label);
},
),
);
}),
],
);
},
loading: () {
// Show minimal loading state or fallback to "All" only
return ListView(
padding: const EdgeInsets.symmetric(horizontal: 16),
scrollDirection: Axis.horizontal,
children: [
_buildFilterChip(
label: 'Tất cả',
isSelected: true,
onTap: () {},
),
],
);
},
error: (error, stack) {
// Show "All" filter only on error
return ListView(
padding: const EdgeInsets.symmetric(horizontal: 16),
scrollDirection: Axis.horizontal,
children: [
_buildFilterChip(
label: 'Tất cả',
isSelected: true,
onTap: () {},
),
],
);
},
),
);
}
@@ -349,3 +351,57 @@ class _OrdersPageState extends ConsumerState<OrdersPage> {
);
}
}
/// Search Bar Delegate for SliverPersistentHeader
class _SearchBarDelegate extends SliverPersistentHeaderDelegate {
_SearchBarDelegate({required this.child});
final Widget child;
@override
double get minExtent => 80; // Height when pinned
@override
double get maxExtent => 80; // Height when expanded
@override
Widget build(
BuildContext context,
double shrinkOffset,
bool overlapsContent,
) {
return child;
}
@override
bool shouldRebuild(_SearchBarDelegate oldDelegate) {
return child != oldDelegate.child;
}
}
/// Filter Pills Delegate for SliverPersistentHeader
class _FilterPillsDelegate extends SliverPersistentHeaderDelegate {
_FilterPillsDelegate({required this.child});
final Widget child;
@override
double get minExtent => 48; // Height when pinned (matches Container height)
@override
double get maxExtent => 48; // Height when expanded (matches Container height)
@override
Widget build(
BuildContext context,
double shrinkOffset,
bool overlapsContent,
) {
return child;
}
@override
bool shouldRebuild(_FilterPillsDelegate oldDelegate) {
return child != oldDelegate.child;
}
}

View File

@@ -6,6 +6,7 @@ library;
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:worker/core/network/dio_client.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/repositories/order_repository_impl.dart';
import 'package:worker/features/orders/domain/repositories/order_repository.dart';
@@ -22,5 +23,6 @@ Future<OrderRemoteDataSource> orderRemoteDataSource(Ref ref) async {
@riverpod
Future<OrderRepository> orderRepository(Ref ref) async {
final remoteDataSource = await ref.watch(orderRemoteDataSourceProvider.future);
return OrderRepositoryImpl(remoteDataSource);
final statusLocalDataSource = OrderStatusLocalDataSource();
return OrderRepositoryImpl(remoteDataSource, statusLocalDataSource);
}

View File

@@ -6,6 +6,7 @@ library;
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:worker/core/network/dio_client.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/repositories/order_repository_impl.dart';
import 'package:worker/features/orders/domain/repositories/order_repository.dart';
@@ -16,7 +17,8 @@ part 'order_repository_provider.g.dart';
Future<OrderRepository> orderRepository(Ref ref) async {
final dioClient = await ref.watch(dioClientProvider.future);
final remoteDataSource = OrderRemoteDataSource(dioClient);
return OrderRepositoryImpl(remoteDataSource);
final statusLocalDataSource = OrderStatusLocalDataSource();
return OrderRepositoryImpl(remoteDataSource, statusLocalDataSource);
}
/// Create Order Provider

View File

@@ -50,7 +50,7 @@ final class OrderRepositoryProvider
}
}
String _$orderRepositoryHash() => r'15efafcf3b545ea52fdc8d0acbd8192ba8f41546';
String _$orderRepositoryHash() => r'f9808aac43686973737a55410e4121ae8332b908';
/// Create Order Provider
///

View File

@@ -4,33 +4,41 @@
library;
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:worker/core/database/models/enums.dart';
import 'package:worker/features/orders/data/datasources/orders_local_datasource.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_status.dart';
import 'package:worker/features/orders/presentation/providers/order_repository_provider.dart';
part 'orders_provider.g.dart';
/// Orders Local Data Source Provider
@riverpod
OrdersLocalDataSource ordersLocalDataSource(Ref ref) {
return OrdersLocalDataSource();
}
/// Orders Provider
///
/// Provides list of all orders from local data source.
/// Provides list of all orders from repository (Clean Architecture).
@riverpod
class Orders extends _$Orders {
@override
Future<List<OrderModel>> build() async {
return await ref.read(ordersLocalDataSourceProvider).getAllOrders();
Future<List<Order>> build() async {
// Fetch orders from repository
try {
final repository = await ref.read(orderRepositoryProvider.future);
return await repository.getOrdersList(
limitStart: 0,
limitPageLength: 0, // 0 = get all
);
} catch (e) {
// Return empty list on error
return [];
}
}
/// Refresh orders
Future<void> refresh() async {
state = const AsyncValue.loading();
state = await AsyncValue.guard(() async {
return await ref.read(ordersLocalDataSourceProvider).getAllOrders();
final repository = await ref.read(orderRepositoryProvider.future);
return await repository.getOrdersList(
limitStart: 0,
limitPageLength: 0,
);
});
}
}
@@ -42,12 +50,12 @@ class Orders extends _$Orders {
@riverpod
class SelectedOrderStatus extends _$SelectedOrderStatus {
@override
OrderStatus? build() {
String? build() {
return null; // Default: show all orders
}
/// Select a status filter
void selectStatus(OrderStatus? status) {
void selectStatus(String? status) {
state = status;
}
@@ -82,7 +90,7 @@ class OrderSearchQuery extends _$OrderSearchQuery {
///
/// Filters orders by selected status and search query.
@riverpod
Future<List<OrderModel>> filteredOrders(Ref ref) async {
Future<List<Order>> filteredOrders(Ref ref) async {
final ordersAsync = ref.watch(ordersProvider);
final selectedStatus = ref.watch(selectedOrderStatusProvider);
final searchQuery = ref.watch(orderSearchQueryProvider);
@@ -102,15 +110,23 @@ Future<List<OrderModel>> filteredOrders(Ref ref) async {
if (searchQuery.isNotEmpty) {
filtered = filtered
.where(
(order) => order.orderNumber.toLowerCase().contains(
(order) => order.name.toLowerCase().contains(
searchQuery.toLowerCase(),
),
)
.toList();
}
// Sort by creation date (newest first)
filtered.sort((a, b) => b.createdAt.compareTo(a.createdAt));
// Sort by transaction date (newest first)
filtered.sort((a, b) {
try {
final aDate = DateTime.parse(a.transactionDate);
final bDate = DateTime.parse(b.transactionDate);
return bDate.compareTo(aDate);
} catch (e) {
return 0; // Keep original order if parsing fails
}
});
return filtered;
},
@@ -123,15 +139,16 @@ Future<List<OrderModel>> filteredOrders(Ref ref) async {
///
/// Returns count of orders for each status.
@riverpod
Future<Map<OrderStatus, int>> ordersCountByStatus(Ref ref) async {
Future<Map<String, int>> ordersCountByStatus(Ref ref) async {
final ordersAsync = ref.watch(ordersProvider);
return ordersAsync.when(
data: (orders) {
final counts = <OrderStatus, int>{};
final counts = <String, int>{};
for (final status in OrderStatus.values) {
counts[status] = orders.where((order) => order.status == status).length;
// Count orders by their status string
for (final order in orders) {
counts[order.status] = (counts[order.status] ?? 0) + 1;
}
return counts;
@@ -152,3 +169,13 @@ Future<int> totalOrdersCount(Ref ref) async {
error: (error, stack) => 0,
);
}
/// Order Status List Provider
///
/// Provides cached order status list with automatic refresh.
/// Uses cache-first strategy with API fallback.
@riverpod
Future<List<OrderStatus>> orderStatusList(Ref ref) async {
final repository = await ref.watch(orderRepositoryProvider.future);
return await repository.getOrderStatusList();
}

View File

@@ -8,74 +8,20 @@ part of 'orders_provider.dart';
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
/// Orders Local Data Source Provider
@ProviderFor(ordersLocalDataSource)
const ordersLocalDataSourceProvider = OrdersLocalDataSourceProvider._();
/// Orders Local Data Source Provider
final class OrdersLocalDataSourceProvider
extends
$FunctionalProvider<
OrdersLocalDataSource,
OrdersLocalDataSource,
OrdersLocalDataSource
>
with $Provider<OrdersLocalDataSource> {
/// Orders Local Data Source Provider
const OrdersLocalDataSourceProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'ordersLocalDataSourceProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$ordersLocalDataSourceHash();
@$internal
@override
$ProviderElement<OrdersLocalDataSource> $createElement(
$ProviderPointer pointer,
) => $ProviderElement(pointer);
@override
OrdersLocalDataSource create(Ref ref) {
return ordersLocalDataSource(ref);
}
/// {@macro riverpod.override_with_value}
Override overrideWithValue(OrdersLocalDataSource value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<OrdersLocalDataSource>(value),
);
}
}
String _$ordersLocalDataSourceHash() =>
r'753fcc2a4000c4c9843fba022d1bf398daba6c7a';
/// Orders Provider
///
/// Provides list of all orders from local data source.
/// Provides list of all orders from repository (Clean Architecture).
@ProviderFor(Orders)
const ordersProvider = OrdersProvider._();
/// Orders Provider
///
/// Provides list of all orders from local data source.
final class OrdersProvider
extends $AsyncNotifierProvider<Orders, List<OrderModel>> {
/// Provides list of all orders from repository (Clean Architecture).
final class OrdersProvider extends $AsyncNotifierProvider<Orders, List<Order>> {
/// Orders Provider
///
/// Provides list of all orders from local data source.
/// Provides list of all orders from repository (Clean Architecture).
const OrdersProvider._()
: super(
from: null,
@@ -95,25 +41,24 @@ final class OrdersProvider
Orders create() => Orders();
}
String _$ordersHash() => r'7d2ae33e528260172495e8360f6879cb6e089766';
String _$ordersHash() => r'1a4712005f0d2fdd2d15e01b6dd9ea2adc428343';
/// Orders Provider
///
/// Provides list of all orders from local data source.
/// Provides list of all orders from repository (Clean Architecture).
abstract class _$Orders extends $AsyncNotifier<List<OrderModel>> {
FutureOr<List<OrderModel>> build();
abstract class _$Orders extends $AsyncNotifier<List<Order>> {
FutureOr<List<Order>> build();
@$mustCallSuper
@override
void runBuild() {
final created = build();
final ref =
this.ref as $Ref<AsyncValue<List<OrderModel>>, List<OrderModel>>;
final ref = this.ref as $Ref<AsyncValue<List<Order>>, List<Order>>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<AsyncValue<List<OrderModel>>, List<OrderModel>>,
AsyncValue<List<OrderModel>>,
AnyNotifier<AsyncValue<List<Order>>, List<Order>>,
AsyncValue<List<Order>>,
Object?,
Object?
>;
@@ -134,7 +79,7 @@ const selectedOrderStatusProvider = SelectedOrderStatusProvider._();
/// Tracks the currently selected order status filter.
/// null means "All" orders.
final class SelectedOrderStatusProvider
extends $NotifierProvider<SelectedOrderStatus, OrderStatus?> {
extends $NotifierProvider<SelectedOrderStatus, String?> {
/// Selected Order Status Provider
///
/// Tracks the currently selected order status filter.
@@ -158,34 +103,34 @@ final class SelectedOrderStatusProvider
SelectedOrderStatus create() => SelectedOrderStatus();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(OrderStatus? value) {
Override overrideWithValue(String? value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<OrderStatus?>(value),
providerOverride: $SyncValueProvider<String?>(value),
);
}
}
String _$selectedOrderStatusHash() =>
r'51834a8660a7f792e4075f76354e8a23a4fe9d7c';
r'24d7f26c87da85b04a6f7ad0691663ef50f9523f';
/// Selected Order Status Provider
///
/// Tracks the currently selected order status filter.
/// null means "All" orders.
abstract class _$SelectedOrderStatus extends $Notifier<OrderStatus?> {
OrderStatus? build();
abstract class _$SelectedOrderStatus extends $Notifier<String?> {
String? build();
@$mustCallSuper
@override
void runBuild() {
final created = build();
final ref = this.ref as $Ref<OrderStatus?, OrderStatus?>;
final ref = this.ref as $Ref<String?, String?>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<OrderStatus?, OrderStatus?>,
OrderStatus?,
AnyNotifier<String?, String?>,
String?,
Object?,
Object?
>;
@@ -274,11 +219,11 @@ const filteredOrdersProvider = FilteredOrdersProvider._();
final class FilteredOrdersProvider
extends
$FunctionalProvider<
AsyncValue<List<OrderModel>>,
List<OrderModel>,
FutureOr<List<OrderModel>>
AsyncValue<List<Order>>,
List<Order>,
FutureOr<List<Order>>
>
with $FutureModifier<List<OrderModel>>, $FutureProvider<List<OrderModel>> {
with $FutureModifier<List<Order>>, $FutureProvider<List<Order>> {
/// Filtered Orders Provider
///
/// Filters orders by selected status and search query.
@@ -298,17 +243,17 @@ final class FilteredOrdersProvider
@$internal
@override
$FutureProviderElement<List<OrderModel>> $createElement(
$FutureProviderElement<List<Order>> $createElement(
$ProviderPointer pointer,
) => $FutureProviderElement(pointer);
@override
FutureOr<List<OrderModel>> create(Ref ref) {
FutureOr<List<Order>> create(Ref ref) {
return filteredOrders(ref);
}
}
String _$filteredOrdersHash() => r'4cc009352d3b09159c0fe107645634c3a4a81a7c';
String _$filteredOrdersHash() => r'04c5c87d7138b66987c8b45f878d445026ec8e19';
/// Orders Count by Status Provider
///
@@ -324,13 +269,11 @@ const ordersCountByStatusProvider = OrdersCountByStatusProvider._();
final class OrdersCountByStatusProvider
extends
$FunctionalProvider<
AsyncValue<Map<OrderStatus, int>>,
Map<OrderStatus, int>,
FutureOr<Map<OrderStatus, int>>
AsyncValue<Map<String, int>>,
Map<String, int>,
FutureOr<Map<String, int>>
>
with
$FutureModifier<Map<OrderStatus, int>>,
$FutureProvider<Map<OrderStatus, int>> {
with $FutureModifier<Map<String, int>>, $FutureProvider<Map<String, int>> {
/// Orders Count by Status Provider
///
/// Returns count of orders for each status.
@@ -350,18 +293,18 @@ final class OrdersCountByStatusProvider
@$internal
@override
$FutureProviderElement<Map<OrderStatus, int>> $createElement(
$FutureProviderElement<Map<String, int>> $createElement(
$ProviderPointer pointer,
) => $FutureProviderElement(pointer);
@override
FutureOr<Map<OrderStatus, int>> create(Ref ref) {
FutureOr<Map<String, int>> create(Ref ref) {
return ordersCountByStatus(ref);
}
}
String _$ordersCountByStatusHash() =>
r'85fe4fb85410855bb434b19fdc05c933c6e76235';
r'f6cd7f4eb47123d8e3bcfc04a82990301f3c2690';
/// Total Orders Count Provider
@@ -400,3 +343,58 @@ final class TotalOrdersCountProvider
}
String _$totalOrdersCountHash() => r'ec1ab3a8d432033aa1f02d28e841e78eba06d63e';
/// Order Status List Provider
///
/// Provides cached order status list with automatic refresh.
/// Uses cache-first strategy with API fallback.
@ProviderFor(orderStatusList)
const orderStatusListProvider = OrderStatusListProvider._();
/// Order Status List Provider
///
/// Provides cached order status list with automatic refresh.
/// Uses cache-first strategy with API fallback.
final class OrderStatusListProvider
extends
$FunctionalProvider<
AsyncValue<List<OrderStatus>>,
List<OrderStatus>,
FutureOr<List<OrderStatus>>
>
with
$FutureModifier<List<OrderStatus>>,
$FutureProvider<List<OrderStatus>> {
/// Order Status List Provider
///
/// Provides cached order status list with automatic refresh.
/// Uses cache-first strategy with API fallback.
const OrderStatusListProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'orderStatusListProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$orderStatusListHash();
@$internal
@override
$FutureProviderElement<List<OrderStatus>> $createElement(
$ProviderPointer pointer,
) => $FutureProviderElement(pointer);
@override
FutureOr<List<OrderStatus>> create(Ref ref) {
return orderStatusList(ref);
}
}
String _$orderStatusListHash() => r'f005726ad238164f7e0dece62476b39fd762e933';

View File

@@ -3,20 +3,18 @@
/// Displays order information in a card format.
library;
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:intl/intl.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/data/models/order_model.dart';
import 'package:worker/features/orders/domain/entities/order.dart';
/// Order Card Widget
///
/// Displays order details in a card with status indicator.
class OrderCard extends StatelessWidget {
/// Order to display
final OrderModel order;
final Order order;
/// Tap callback
final VoidCallback? onTap;
@@ -50,7 +48,7 @@ class OrderCard extends StatelessWidget {
children: [
// Order number
Text(
'#${order.orderNumber}',
'#${order.name}',
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w700,
@@ -60,7 +58,7 @@ class OrderCard extends StatelessWidget {
// Amount
Text(
currencyFormatter.format(order.finalAmount),
currencyFormatter.format(order.grandTotal),
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w700,
@@ -73,18 +71,13 @@ class OrderCard extends StatelessWidget {
const SizedBox(height: 12),
// Order details
_buildDetailRow('Ngày đặt:', _formatDate(order.createdAt)),
_buildDetailRow('Ngày đặt:', _formatDate(order.transactionDate)),
const SizedBox(height: 6),
_buildDetailRow(
'Ngày giao:',
order.expectedDeliveryDate != null
? _formatDate(order.expectedDeliveryDate!)
: 'Chưa xác định',
),
_buildDetailRow('Ngày giao:', _formatDate(order.deliveryDate)),
const SizedBox(height: 6),
_buildDetailRow('Địa chỉ:', _getShortAddress()),
_buildDetailRow('Địa chỉ:', order.address),
const SizedBox(height: 12),
// Status badge
@@ -118,100 +111,50 @@ class OrderCard extends StatelessWidget {
/// Build status badge
Widget _buildStatusBadge() {
final statusColor = _getStatusColor();
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: _getStatusColor(order.status).withValues(alpha: 0.1),
color: statusColor.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: _getStatusColor(order.status).withValues(alpha: 0.3),
color: statusColor.withValues(alpha: 0.3),
width: 1,
),
),
child: Text(
_getStatusText(order.status),
order.status,
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: _getStatusColor(order.status),
color: statusColor,
),
),
);
}
/// Get status color
Color _getStatusColor(OrderStatus status) {
switch (status) {
case OrderStatus.draft:
return AppColors.grey500;
case OrderStatus.pending:
return const Color(0xFFF59E0B); // warning/pending color
case OrderStatus.confirmed:
return const Color(0xFFF59E0B); // warning/pending color
case OrderStatus.processing:
return AppColors.info;
case OrderStatus.shipped:
return const Color(0xFF3B82F6); // blue
case OrderStatus.delivered:
return const Color(0xFF10B981); // green
case OrderStatus.completed:
return AppColors.success;
case OrderStatus.cancelled:
return AppColors.danger;
case OrderStatus.refunded:
return const Color(0xFFF97316); // orange
}
}
/// Get status text in Vietnamese
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 giao';
case OrderStatus.delivered:
return 'Đã giao';
case OrderStatus.completed:
return 'Hoàn thành';
case OrderStatus.cancelled:
return 'Đã hủy';
case OrderStatus.refunded:
return 'Đã hoàn tiền';
}
/// Get status color from API status_color field
Color _getStatusColor() {
// Parse statusColor from API (Warning, Success, Danger, Info, Secondary)
final statusColorEnum = StatusColor.values.firstWhere(
(e) => e.name.toLowerCase() == order.statusColor.toLowerCase(),
orElse: () => StatusColor.secondary,
);
return statusColorEnum.color;
}
/// Format date to dd/MM/yyyy
String _formatDate(DateTime date) {
return DateFormat('dd/MM/yyyy').format(date);
}
/// Get short address (city or district, city)
String _getShortAddress() {
if (order.shippingAddress == null) {
return 'Chưa có địa chỉ';
String _formatDate(String? dateString) {
if (dateString == null || dateString.isEmpty) {
return 'Chưa xác định';
}
try {
final addressJson = jsonDecode(order.shippingAddress!);
final city = addressJson['city'] as String?;
final district = addressJson['district'] as String?;
if (district != null && city != null) {
return '$district, $city';
} else if (city != null) {
return city;
} else {
return 'Chưa có địa chỉ';
}
final date = DateTime.parse(dateString);
return DateFormat('dd/MM/yyyy').format(date);
} catch (e) {
return 'Chưa có địa chỉ';
return dateString;
}
}
}