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');
}
}