list orders
This commit is contained in:
@@ -233,7 +233,19 @@ class ApiConstants {
|
||||
/// Returns: { "message": { "file_url": "...", "file_name": "...", ... } }
|
||||
static const String uploadFile = '/upload_file';
|
||||
|
||||
/// Get user's orders
|
||||
/// Get list of orders (requires sid and csrf_token)
|
||||
/// POST /api/method/building_material.building_material.api.sales_order.get_list
|
||||
/// Body: { "limit_start": 0, "limit_page_length": 0 }
|
||||
/// Returns: { "message": [...] }
|
||||
static const String getOrdersList = '/building_material.building_material.api.sales_order.get_list';
|
||||
|
||||
/// Get order details (requires sid and csrf_token)
|
||||
/// POST /api/method/building_material.building_material.api.sales_order.get_detail
|
||||
/// Body: { "name": "SAL-ORD-2025-00058-1" }
|
||||
/// Returns: { "message": {...} }
|
||||
static const String getOrderDetail = '/building_material.building_material.api.sales_order.get_detail';
|
||||
|
||||
/// Get user's orders (legacy endpoint - may be deprecated)
|
||||
/// GET /orders?status={status}&page={page}&limit={limit}
|
||||
static const String getOrders = '/orders';
|
||||
|
||||
|
||||
@@ -61,6 +61,9 @@ class HiveBoxNames {
|
||||
static const String cityBox = 'city_box';
|
||||
static const String wardBox = 'ward_box';
|
||||
|
||||
/// Order status list cache
|
||||
static const String orderStatusBox = 'order_status_box';
|
||||
|
||||
/// Get all box names for initialization
|
||||
static List<String> get allBoxes => [
|
||||
userBox,
|
||||
@@ -73,6 +76,7 @@ class HiveBoxNames {
|
||||
rewardsBox,
|
||||
cityBox,
|
||||
wardBox,
|
||||
orderStatusBox,
|
||||
settingsBox,
|
||||
cacheBox,
|
||||
syncStateBox,
|
||||
@@ -134,8 +138,9 @@ class HiveTypeIds {
|
||||
static const int addressModel = 30;
|
||||
static const int cityModel = 31;
|
||||
static const int wardModel = 32;
|
||||
static const int orderStatusModel = 62;
|
||||
|
||||
// Enums (33-62)
|
||||
// Enums (33-61)
|
||||
static const int userRole = 33;
|
||||
static const int userStatus = 34;
|
||||
static const int loyaltyTier = 35;
|
||||
|
||||
@@ -168,6 +168,9 @@ class HiveService {
|
||||
// Location boxes (non-sensitive) - caches cities and wards for address forms
|
||||
Hive.openBox<dynamic>(HiveBoxNames.cityBox),
|
||||
Hive.openBox<dynamic>(HiveBoxNames.wardBox),
|
||||
|
||||
// Order status box (non-sensitive) - caches order status list from API
|
||||
Hive.openBox<dynamic>(HiveBoxNames.orderStatusBox),
|
||||
]);
|
||||
|
||||
// Open potentially encrypted boxes (sensitive data)
|
||||
|
||||
141
lib/core/enums/status_color.dart
Normal file
141
lib/core/enums/status_color.dart
Normal file
@@ -0,0 +1,141 @@
|
||||
/// Status Color Enum
|
||||
///
|
||||
/// Defines status types with their associated color values.
|
||||
/// Used for status badges, alerts, and other UI elements that need
|
||||
/// consistent color coding across the app.
|
||||
library;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Status Color Enum
|
||||
///
|
||||
/// Each status type has an associated color value.
|
||||
enum StatusColor {
|
||||
/// Warning status - Yellow/Orange
|
||||
/// Used for cautionary states, pending actions, or items requiring attention
|
||||
warning(Color(0xFFFFC107)),
|
||||
|
||||
/// Info status - Primary Blue
|
||||
/// Used for informational states, neutral notifications, or general information
|
||||
info(Color(0xFF005B9A)),
|
||||
|
||||
/// Danger status - Red
|
||||
/// Used for error states, critical alerts, or destructive actions
|
||||
danger(Color(0xFFDC3545)),
|
||||
|
||||
/// Success status - Green
|
||||
/// Used for successful operations, completed states, or positive confirmations
|
||||
success(Color(0xFF28A745)),
|
||||
|
||||
/// Secondary status - Light Grey
|
||||
/// Used for secondary information, disabled states, or less important elements
|
||||
secondary(Color(0xFFE5E7EB));
|
||||
|
||||
/// Constructor
|
||||
const StatusColor(this.color);
|
||||
|
||||
/// The color value associated with this status
|
||||
final Color color;
|
||||
|
||||
/// Get a lighter version of the color (with opacity)
|
||||
/// Useful for backgrounds and subtle highlights
|
||||
Color get light => color.withValues(alpha: 0.1);
|
||||
|
||||
/// Get a slightly darker version for borders
|
||||
/// Useful for card borders and dividers
|
||||
Color get border => color.withValues(alpha: 0.3);
|
||||
|
||||
/// Get the color with custom opacity
|
||||
Color withOpacity(double opacity) => color.withValues(alpha: opacity);
|
||||
|
||||
/// Convert from string name (case-insensitive)
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// final status = StatusColor.fromString('warning');
|
||||
/// // Returns StatusColor.warning
|
||||
/// ```
|
||||
static StatusColor? fromString(String name) {
|
||||
try {
|
||||
return StatusColor.values.firstWhere(
|
||||
(e) => e.name.toLowerCase() == name.toLowerCase(),
|
||||
);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Get status color from order status string
|
||||
///
|
||||
/// Maps common order status strings to appropriate colors.
|
||||
/// Returns null if no mapping exists.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// final color = StatusColor.fromOrderStatus('Processing');
|
||||
/// // Returns StatusColor.warning
|
||||
/// ```
|
||||
static StatusColor? fromOrderStatus(String status) {
|
||||
final statusLower = status.toLowerCase();
|
||||
|
||||
// Success states
|
||||
if (statusLower.contains('completed') ||
|
||||
statusLower.contains('delivered') ||
|
||||
statusLower.contains('paid') ||
|
||||
statusLower.contains('approved')) {
|
||||
return StatusColor.success;
|
||||
}
|
||||
|
||||
// Warning/Pending states
|
||||
if (statusLower.contains('pending') ||
|
||||
statusLower.contains('processing') ||
|
||||
statusLower.contains('shipping') ||
|
||||
statusLower.contains('reviewing')) {
|
||||
return StatusColor.warning;
|
||||
}
|
||||
|
||||
// Danger/Error states
|
||||
if (statusLower.contains('cancelled') ||
|
||||
statusLower.contains('rejected') ||
|
||||
statusLower.contains('failed') ||
|
||||
statusLower.contains('expired')) {
|
||||
return StatusColor.danger;
|
||||
}
|
||||
|
||||
// Info states
|
||||
if (statusLower.contains('draft') ||
|
||||
statusLower.contains('sent') ||
|
||||
statusLower.contains('viewed')) {
|
||||
return StatusColor.info;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Get status color from payment status string
|
||||
///
|
||||
/// Maps common payment status strings to appropriate colors.
|
||||
/// Returns null if no mapping exists.
|
||||
static StatusColor? fromPaymentStatus(String status) {
|
||||
final statusLower = status.toLowerCase();
|
||||
|
||||
// Success states
|
||||
if (statusLower.contains('completed') || statusLower.contains('paid')) {
|
||||
return StatusColor.success;
|
||||
}
|
||||
|
||||
// Warning/Pending states
|
||||
if (statusLower.contains('pending') || statusLower.contains('processing')) {
|
||||
return StatusColor.warning;
|
||||
}
|
||||
|
||||
// Danger/Error states
|
||||
if (statusLower.contains('failed') ||
|
||||
statusLower.contains('rejected') ||
|
||||
statusLower.contains('refunded')) {
|
||||
return StatusColor.danger;
|
||||
}
|
||||
|
||||
return StatusColor.info;
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
''';
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
50
lib/features/orders/data/models/order_status_model.g.dart
Normal file
50
lib/features/orders/data/models/order_status_model.g.dart
Normal 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;
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -50,7 +50,7 @@ final class OrderRepositoryProvider
|
||||
}
|
||||
}
|
||||
|
||||
String _$orderRepositoryHash() => r'15efafcf3b545ea52fdc8d0acbd8192ba8f41546';
|
||||
String _$orderRepositoryHash() => r'f9808aac43686973737a55410e4121ae8332b908';
|
||||
|
||||
/// Create Order Provider
|
||||
///
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ import 'package:worker/features/loyalty/data/models/redeemed_gift_model.dart';
|
||||
import 'package:worker/features/orders/data/models/invoice_model.dart';
|
||||
import 'package:worker/features/orders/data/models/order_item_model.dart';
|
||||
import 'package:worker/features/orders/data/models/order_model.dart';
|
||||
import 'package:worker/features/orders/data/models/order_status_model.dart';
|
||||
import 'package:worker/features/orders/data/models/payment_line_model.dart';
|
||||
import 'package:worker/features/products/data/models/category_model.dart';
|
||||
import 'package:worker/features/products/data/models/product_model.dart';
|
||||
@@ -67,6 +68,7 @@ extension HiveRegistrar on HiveInterface {
|
||||
registerAdapter(OrderItemModelAdapter());
|
||||
registerAdapter(OrderModelAdapter());
|
||||
registerAdapter(OrderStatusAdapter());
|
||||
registerAdapter(OrderStatusModelAdapter());
|
||||
registerAdapter(PaymentLineModelAdapter());
|
||||
registerAdapter(PaymentMethodAdapter());
|
||||
registerAdapter(PaymentReminderModelAdapter());
|
||||
@@ -125,6 +127,7 @@ extension IsolatedHiveRegistrar on IsolatedHiveInterface {
|
||||
registerAdapter(OrderItemModelAdapter());
|
||||
registerAdapter(OrderModelAdapter());
|
||||
registerAdapter(OrderStatusAdapter());
|
||||
registerAdapter(OrderStatusModelAdapter());
|
||||
registerAdapter(PaymentLineModelAdapter());
|
||||
registerAdapter(PaymentMethodAdapter());
|
||||
registerAdapter(PaymentReminderModelAdapter());
|
||||
|
||||
Reference in New Issue
Block a user