From 75d6507719ec643af237e7761d70fde18e4f7ca9 Mon Sep 17 00:00:00 2001 From: Phuoc Nguyen Date: Mon, 24 Nov 2025 16:25:54 +0700 Subject: [PATCH] list orders --- docs/order.sh | 36 ++ docs/order_model_update_summary.md | 81 ++++ lib/core/constants/api_constants.dart | 14 +- lib/core/constants/storage_constants.dart | 7 +- lib/core/database/hive_service.dart | 3 + lib/core/enums/status_color.dart | 141 +++++++ .../datasources/order_remote_datasource.dart | 72 ++++ .../order_status_local_datasource.dart | 47 +++ .../datasources/orders_local_datasource.dart | 215 ----------- .../orders/data/models/order_model.dart | 216 +++++------ .../orders/data/models/order_model.g.dart | 75 ++-- .../data/models/order_status_model.dart | 24 +- .../data/models/order_status_model.g.dart | 50 +++ .../repositories/order_repository_impl.dart | 47 ++- .../orders/domain/entities/order.dart | 348 ++++-------------- .../domain/repositories/order_repository.dart | 7 + .../presentation/pages/orders_page.dart | 224 ++++++----- .../providers/order_data_providers.dart | 4 +- .../providers/order_repository_provider.dart | 4 +- .../order_repository_provider.g.dart | 2 +- .../providers/orders_provider.dart | 73 ++-- .../providers/orders_provider.g.dart | 180 +++++---- .../presentation/widgets/order_card.dart | 113 ++---- lib/hive_registrar.g.dart | 3 + 24 files changed, 1004 insertions(+), 982 deletions(-) create mode 100644 docs/order_model_update_summary.md create mode 100644 lib/core/enums/status_color.dart create mode 100644 lib/features/orders/data/datasources/order_status_local_datasource.dart delete mode 100644 lib/features/orders/data/datasources/orders_local_datasource.dart create mode 100644 lib/features/orders/data/models/order_status_model.g.dart diff --git a/docs/order.sh b/docs/order.sh index aa2ec69..19fc5f0 100644 --- a/docs/order.sh +++ b/docs/order.sh @@ -129,6 +129,42 @@ curl --location 'https://land.dbiz.com//api/method/upload_file' \ --form 'optimize="true"' +#get list order +curl --location 'https://land.dbiz.com//api/method/building_material.building_material.api.sales_order.get_list' \ +--header 'Cookie: sid=a0cbe3ea6f9a7e9cf083bbe3139eada68d2357eac0167bcc66cda17d; full_name=Ha%20Duy%20Lam; sid=a0cbe3ea6f9a7e9cf083bbe3139eada68d2357eac0167bcc66cda17d; system_user=yes; user_id=lamhd%40gmail.com; user_image=/files/avatar_0986788766_1763627962.jpg' \ +--header 'X-Frappe-Csrf-Token: 6ff3be4d1f887dbebf86ba4502b05d94b30c0b0569de49b74a7171a9' \ +--header 'Content-Type: application/json' \ +--data '{ + "limit_start" : 0, + "limit_page_length" : 0 + +}' + +#response list order +{ + "message": [ + { + "name": "SAL-ORD-2025-00107", + "transaction_date": "2025-11-24", + "delivery_date": "2025-11-24", + "address": "123 add dad", + "grand_total": 3355443.2, + "status": "Chờ phê duyệt", + "status_color": "Warning" + }, + { + "name": "SAL-ORD-2025-00106", + "transaction_date": "2025-11-24", + "delivery_date": "2025-11-24", + "address": "123 add dad", + "grand_total": 3355443.2, + "status": "Chờ phê duyệt", + "status_color": "Warning" + }, + ... + ] +} + #order detail curl --location 'https://land.dbiz.com//api/method/building_material.building_material.api.sales_order.get_detail' \ --header 'Cookie: sid=a0cbe3ea6f9a7e9cf083bbe3139eada68d2357eac0167bcc66cda17d; sid=a0cbe3ea6f9a7e9cf083bbe3139eada68d2357eac0167bcc66cda17d' \ diff --git a/docs/order_model_update_summary.md b/docs/order_model_update_summary.md new file mode 100644 index 0000000..ce13c97 --- /dev/null +++ b/docs/order_model_update_summary.md @@ -0,0 +1,81 @@ +# Order Model API Integration Update + +## Summary +Updated OrderModel and orders_provider to match the simplified API response structure from the ERPNext/Frappe backend. + +## API Response Structure +```json +{ + "message": [ + { + "name": "SAL-ORD-2025-00107", + "transaction_date": "2025-11-24", + "delivery_date": "2025-11-24", + "address": "123 add dad", + "grand_total": 3355443.2, + "status": "Chờ phê duyệt", + "status_color": "Warning" + } + ] +} +``` + +## Changes Made + +### 1. OrderModel (`lib/features/orders/data/models/order_model.dart`) +**New Fields Added:** +- `statusColor` (HiveField 18): Stores API status color (Warning, Success, Danger, etc.) +- `transactionDate` (HiveField 19): Transaction date from API +- `addressString` (HiveField 20): Simple string address from API + +**Updated Methods:** +- `fromJson()`: Made fields more nullable, added new field mappings +- `toJson()`: Added new fields to output +- Constructor: Added new optional parameters + +### 2. Orders Provider (`lib/features/orders/presentation/providers/orders_provider.dart`) +**API Field Mapping:** +```dart +{ + 'order_id': json['name'], + 'order_number': json['name'], + 'status': _mapStatusFromApi(json['status']), + 'total_amount': json['grand_total'], + 'final_amount': json['grand_total'], + 'expected_delivery_date': json['delivery_date'], + 'transaction_date': json['transaction_date'], + 'address_string': json['address'], + 'status_color': json['status_color'], + 'created_at': json['transaction_date'], +} +``` + +**Status Mapping:** +- "Chờ phê duyệt" / "Pending approval" → `pending` +- "Đang xử lý" / "Processing" → `processing` +- "Đang giao" / "Shipped" → `shipped` +- "Hoàn thành" / "Completed" → `completed` +- "Đã hủy" / "Cancelled" / "Rejected" → `cancelled` + +### 3. Order Card Widget (`lib/features/orders/presentation/widgets/order_card.dart`) +**Display Updates:** +- Uses `transactionDate` if available, falls back to `createdAt` +- Uses `addressString` directly from API instead of parsing JSON + +## Benefits +1. **Simpler mapping**: Direct field mapping without complex transformations +2. **API consistency**: Matches actual backend response structure +3. **Better performance**: No need to parse JSON addresses for list view +4. **Status colors**: API-provided colors ensure UI consistency with backend + +## API Endpoint +``` +POST /api/method/building_material.building_material.api.sales_order.get_list +Body: { "limit_start": 0, "limit_page_length": 0 } +``` + +## Testing Notes +- Ensure API returns all expected fields +- Verify Vietnamese status strings are correctly mapped +- Check that dates are in ISO format (YYYY-MM-DD) +- Confirm status_color values match StatusColor enum (Warning, Success, Danger, Info, Secondary) diff --git a/lib/core/constants/api_constants.dart b/lib/core/constants/api_constants.dart index 9f51028..e40e1c9 100644 --- a/lib/core/constants/api_constants.dart +++ b/lib/core/constants/api_constants.dart @@ -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'; diff --git a/lib/core/constants/storage_constants.dart b/lib/core/constants/storage_constants.dart index c8a4b7a..92a3ad2 100644 --- a/lib/core/constants/storage_constants.dart +++ b/lib/core/constants/storage_constants.dart @@ -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 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; diff --git a/lib/core/database/hive_service.dart b/lib/core/database/hive_service.dart index bbffac0..f51f761 100644 --- a/lib/core/database/hive_service.dart +++ b/lib/core/database/hive_service.dart @@ -168,6 +168,9 @@ class HiveService { // Location boxes (non-sensitive) - caches cities and wards for address forms Hive.openBox(HiveBoxNames.cityBox), Hive.openBox(HiveBoxNames.wardBox), + + // Order status box (non-sensitive) - caches order status list from API + Hive.openBox(HiveBoxNames.orderStatusBox), ]); // Open potentially encrypted boxes (sensitive data) diff --git a/lib/core/enums/status_color.dart b/lib/core/enums/status_color.dart new file mode 100644 index 0000000..9752491 --- /dev/null +++ b/lib/core/enums/status_color.dart @@ -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; + } +} diff --git a/lib/features/orders/data/datasources/order_remote_datasource.dart b/lib/features/orders/data/datasources/order_remote_datasource.dart index b65a0ac..4c7d15a 100644 --- a/lib/features/orders/data/datasources/order_remote_datasource.dart +++ b/lib/features/orders/data/datasources/order_remote_datasource.dart @@ -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>> getOrdersList({ + int limitStart = 0, + int limitPageLength = 0, + }) async { + try { + final response = await _dioClient.post>( + '${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>(); + } 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> getOrderDetail(String orderName) async { + try { + final response = await _dioClient.post>( + '${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) { + throw Exception('Expected map but got ${message.runtimeType}'); + } + + return message; + } catch (e) { + throw Exception('Failed to get order detail: $e'); + } + } } diff --git a/lib/features/orders/data/datasources/order_status_local_datasource.dart b/lib/features/orders/data/datasources/order_status_local_datasource.dart new file mode 100644 index 0000000..18063c4 --- /dev/null +++ b/lib/features/orders/data/datasources/order_status_local_datasource.dart @@ -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 get _box => Hive.box(HiveBoxNames.orderStatusBox); + + /// Save order status list to cache + Future cacheStatusList(List 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 getCachedStatusList() { + try { + final values = _box.values.whereType().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 clearCache() async { + await _box.clear(); + } +} diff --git a/lib/features/orders/data/datasources/orders_local_datasource.dart b/lib/features/orders/data/datasources/orders_local_datasource.dart deleted file mode 100644 index 31d6220..0000000 --- a/lib/features/orders/data/datasources/orders_local_datasource.dart +++ /dev/null @@ -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> 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)) - .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> 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> 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 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" - } - ] - '''; -} diff --git a/lib/features/orders/data/models/order_model.dart b/lib/features/orders/data/models/order_model.dart index b204821..2ee601b 100644 --- a/lib/features/orders/data/models/order_model.dart +++ b/lib/features/orders/data/models/order_model.dart @@ -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 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 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? get shippingAddressMap { - if (shippingAddress == null) return null; + /// Get parsed transaction date + DateTime? get transactionDateTime { try { - return jsonDecode(shippingAddress!) as Map; + return DateTime.parse(transactionDate); } catch (e) { return null; } } - Map? get billingAddressMap { - if (billingAddress == null) return null; + /// Get parsed delivery date + DateTime? get deliveryDateTime { try { - return jsonDecode(billingAddress!) as Map; + 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, + ); + } } diff --git a/lib/features/orders/data/models/order_model.g.dart b/lib/features/orders/data/models/order_model.g.dart index 39e6273..4ca174b 100644 --- a/lib/features/orders/data/models/order_model.g.dart +++ b/lib/features/orders/data/models/order_model.g.dart @@ -17,67 +17,34 @@ class OrderModelAdapter extends TypeAdapter { 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 diff --git a/lib/features/orders/data/models/order_status_model.dart b/lib/features/orders/data/models/order_status_model.dart index fff60ba..f4cd208 100644 --- a/lib/features/orders/data/models/order_status_model.dart +++ b/lib/features/orders/data/models/order_status_model.dart @@ -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 get props => [status, label, color, index]; } diff --git a/lib/features/orders/data/models/order_status_model.g.dart b/lib/features/orders/data/models/order_status_model.g.dart new file mode 100644 index 0000000..ea80669 --- /dev/null +++ b/lib/features/orders/data/models/order_status_model.g.dart @@ -0,0 +1,50 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'order_status_model.dart'; + +// ************************************************************************** +// TypeAdapterGenerator +// ************************************************************************** + +class OrderStatusModelAdapter extends TypeAdapter { + @override + final typeId = 62; + + @override + OrderStatusModel read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + 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; +} diff --git a/lib/features/orders/data/repositories/order_repository_impl.dart b/lib/features/orders/data/repositories/order_repository_impl.dart index ce176d0..417e3e7 100644 --- a/lib/features/orders/data/repositories/order_repository_impl.dart +++ b/lib/features/orders/data/repositories/order_repository_impl.dart @@ -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> 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> 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'); } } diff --git a/lib/features/orders/domain/entities/order.dart b/lib/features/orders/domain/entities/order.dart index 3030bc3..8abf6e8 100644 --- a/lib/features/orders/domain/entities/order.dart +++ b/lib/features/orders/domain/entities/order.dart @@ -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 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 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 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)'; } } diff --git a/lib/features/orders/domain/repositories/order_repository.dart b/lib/features/orders/domain/repositories/order_repository.dart index 425100a..c49ef7f 100644 --- a/lib/features/orders/domain/repositories/order_repository.dart +++ b/lib/features/orders/domain/repositories/order_repository.dart @@ -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> getOrdersList({ + int limitStart = 0, + int limitPageLength = 0, + }); + /// Get list of available order statuses Future> getOrderStatusList(); diff --git a/lib/features/orders/presentation/pages/orders_page.dart b/lib/features/orders/presentation/pages/orders_page.dart index c75b5b7..4f0d6ef 100644 --- a/lib/features/orders/presentation/pages/orders_page.dart +++ b/lib/features/orders/presentation/pages/orders_page.dart @@ -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 { }, 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 { 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 { ); } - /// 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 { ); } } + +/// 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; + } +} diff --git a/lib/features/orders/presentation/providers/order_data_providers.dart b/lib/features/orders/presentation/providers/order_data_providers.dart index 1ca7334..1b24f86 100644 --- a/lib/features/orders/presentation/providers/order_data_providers.dart +++ b/lib/features/orders/presentation/providers/order_data_providers.dart @@ -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(Ref ref) async { @riverpod Future orderRepository(Ref ref) async { final remoteDataSource = await ref.watch(orderRemoteDataSourceProvider.future); - return OrderRepositoryImpl(remoteDataSource); + final statusLocalDataSource = OrderStatusLocalDataSource(); + return OrderRepositoryImpl(remoteDataSource, statusLocalDataSource); } diff --git a/lib/features/orders/presentation/providers/order_repository_provider.dart b/lib/features/orders/presentation/providers/order_repository_provider.dart index 6c0daea..170d40e 100644 --- a/lib/features/orders/presentation/providers/order_repository_provider.dart +++ b/lib/features/orders/presentation/providers/order_repository_provider.dart @@ -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(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 diff --git a/lib/features/orders/presentation/providers/order_repository_provider.g.dart b/lib/features/orders/presentation/providers/order_repository_provider.g.dart index 56761a8..3927e13 100644 --- a/lib/features/orders/presentation/providers/order_repository_provider.g.dart +++ b/lib/features/orders/presentation/providers/order_repository_provider.g.dart @@ -50,7 +50,7 @@ final class OrderRepositoryProvider } } -String _$orderRepositoryHash() => r'15efafcf3b545ea52fdc8d0acbd8192ba8f41546'; +String _$orderRepositoryHash() => r'f9808aac43686973737a55410e4121ae8332b908'; /// Create Order Provider /// diff --git a/lib/features/orders/presentation/providers/orders_provider.dart b/lib/features/orders/presentation/providers/orders_provider.dart index 537542b..4093623 100644 --- a/lib/features/orders/presentation/providers/orders_provider.dart +++ b/lib/features/orders/presentation/providers/orders_provider.dart @@ -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> build() async { - return await ref.read(ordersLocalDataSourceProvider).getAllOrders(); + Future> 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 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> filteredOrders(Ref ref) async { +Future> filteredOrders(Ref ref) async { final ordersAsync = ref.watch(ordersProvider); final selectedStatus = ref.watch(selectedOrderStatusProvider); final searchQuery = ref.watch(orderSearchQueryProvider); @@ -102,15 +110,23 @@ Future> 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> filteredOrders(Ref ref) async { /// /// Returns count of orders for each status. @riverpod -Future> ordersCountByStatus(Ref ref) async { +Future> ordersCountByStatus(Ref ref) async { final ordersAsync = ref.watch(ordersProvider); return ordersAsync.when( data: (orders) { - final counts = {}; + final counts = {}; - 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 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> orderStatusList(Ref ref) async { + final repository = await ref.watch(orderRepositoryProvider.future); + return await repository.getOrderStatusList(); +} diff --git a/lib/features/orders/presentation/providers/orders_provider.g.dart b/lib/features/orders/presentation/providers/orders_provider.g.dart index 1ef7cdf..a6d40bd 100644 --- a/lib/features/orders/presentation/providers/orders_provider.g.dart +++ b/lib/features/orders/presentation/providers/orders_provider.g.dart @@ -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 { - /// 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 $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(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> { +/// Provides list of all orders from repository (Clean Architecture). +final class OrdersProvider extends $AsyncNotifierProvider> { /// 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> { - FutureOr> build(); +abstract class _$Orders extends $AsyncNotifier> { + FutureOr> build(); @$mustCallSuper @override void runBuild() { final created = build(); - final ref = - this.ref as $Ref>, List>; + final ref = this.ref as $Ref>, List>; final element = ref.element as $ClassProviderElement< - AnyNotifier>, List>, - AsyncValue>, + AnyNotifier>, List>, + AsyncValue>, 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 { + extends $NotifierProvider { /// 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(value), + providerOverride: $SyncValueProvider(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? build(); +abstract class _$SelectedOrderStatus extends $Notifier { + String? build(); @$mustCallSuper @override void runBuild() { final created = build(); - final ref = this.ref as $Ref; + final ref = this.ref as $Ref; final element = ref.element as $ClassProviderElement< - AnyNotifier, - OrderStatus?, + AnyNotifier, + String?, Object?, Object? >; @@ -274,11 +219,11 @@ const filteredOrdersProvider = FilteredOrdersProvider._(); final class FilteredOrdersProvider extends $FunctionalProvider< - AsyncValue>, - List, - FutureOr> + AsyncValue>, + List, + FutureOr> > - with $FutureModifier>, $FutureProvider> { + with $FutureModifier>, $FutureProvider> { /// Filtered Orders Provider /// /// Filters orders by selected status and search query. @@ -298,17 +243,17 @@ final class FilteredOrdersProvider @$internal @override - $FutureProviderElement> $createElement( + $FutureProviderElement> $createElement( $ProviderPointer pointer, ) => $FutureProviderElement(pointer); @override - FutureOr> create(Ref ref) { + FutureOr> 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, - FutureOr> + AsyncValue>, + Map, + FutureOr> > - with - $FutureModifier>, - $FutureProvider> { + with $FutureModifier>, $FutureProvider> { /// Orders Count by Status Provider /// /// Returns count of orders for each status. @@ -350,18 +293,18 @@ final class OrdersCountByStatusProvider @$internal @override - $FutureProviderElement> $createElement( + $FutureProviderElement> $createElement( $ProviderPointer pointer, ) => $FutureProviderElement(pointer); @override - FutureOr> create(Ref ref) { + FutureOr> 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, + FutureOr> + > + with + $FutureModifier>, + $FutureProvider> { + /// 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> $createElement( + $ProviderPointer pointer, + ) => $FutureProviderElement(pointer); + + @override + FutureOr> create(Ref ref) { + return orderStatusList(ref); + } +} + +String _$orderStatusListHash() => r'f005726ad238164f7e0dece62476b39fd762e933'; diff --git a/lib/features/orders/presentation/widgets/order_card.dart b/lib/features/orders/presentation/widgets/order_card.dart index 9e6463e..f9794c8 100644 --- a/lib/features/orders/presentation/widgets/order_card.dart +++ b/lib/features/orders/presentation/widgets/order_card.dart @@ -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; } } } diff --git a/lib/hive_registrar.g.dart b/lib/hive_registrar.g.dart index e7bfaee..351c7d0 100644 --- a/lib/hive_registrar.g.dart +++ b/lib/hive_registrar.g.dart @@ -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());