Compare commits
3 Commits
42d91a5a99
...
1851d60038
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1851d60038 | ||
|
|
75d6507719 | ||
|
|
354df3ad01 |
152
docs/order.sh
152
docs/order.sh
@@ -116,3 +116,155 @@ curl --location 'https://land.dbiz.com//api/method/building_material.building_ma
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#upload bill
|
||||
curl --location 'https://land.dbiz.com//api/method/upload_file' \
|
||||
--header 'Cookie: sid=a0cbe3ea6f9a7e9cf083bbe3139eada68d2357eac0167bcc66cda17d; sid=a0cbe3ea6f9a7e9cf083bbe3139eada68d2357eac0167bcc66cda17d' \
|
||||
--header 'X-Frappe-Csrf-Token: 6ff3be4d1f887dbebf86ba4502b05d94b30c0b0569de49b74a7171a9' \
|
||||
--form 'file=@"/C:/Users/tiennld/Downloads/logo_crm.png"' \
|
||||
--form 'is_private="1"' \
|
||||
--form 'folder="Home/Attachments"' \
|
||||
--form 'doctype="Sales Order"' \
|
||||
--form 'docname="SAL-ORD-2025-00058-1"' \
|
||||
--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' \
|
||||
--header 'X-Frappe-Csrf-Token: 6ff3be4d1f887dbebf86ba4502b05d94b30c0b0569de49b74a7171a9' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"name" : "SAL-ORD-2025-00058-1"
|
||||
}'
|
||||
|
||||
#response order detail
|
||||
{
|
||||
"message": {
|
||||
"order": {
|
||||
"name": "SAL-ORD-2025-00107",
|
||||
"customer": "test - 1",
|
||||
"transaction_date": "2025-11-24",
|
||||
"delivery_date": "2025-11-24",
|
||||
"status": "Chờ phê duyệt",
|
||||
"status_color": "Warning",
|
||||
"total_qty": 2.56,
|
||||
"total": 3355443.2,
|
||||
"grand_total": 3355443.2,
|
||||
"total_remaining": 0,
|
||||
"description": "Order from mobile app",
|
||||
"contract_request": false,
|
||||
"ignore_pricing_rule": false,
|
||||
"rejection_reason": null,
|
||||
"is_allow_cancel": true
|
||||
},
|
||||
"billing_address": {
|
||||
"name": "phuoc-Billing-3",
|
||||
"address_title": "phuoc",
|
||||
"address_line1": "123 add dad",
|
||||
"phone": "0123123123",
|
||||
"email": "123@gmail.com",
|
||||
"fax": null,
|
||||
"tax_code": "064521840",
|
||||
"city_code": "19",
|
||||
"ward_code": "01936",
|
||||
"city_name": "Tỉnh Thái Nguyên",
|
||||
"ward_name": "Xã Nà Phặc",
|
||||
"is_allow_edit": true
|
||||
},
|
||||
"shipping_address": {
|
||||
"name": "phuoc-Billing-3",
|
||||
"address_title": "phuoc",
|
||||
"address_line1": "123 add dad",
|
||||
"phone": "0123123123",
|
||||
"email": "123@gmail.com",
|
||||
"fax": null,
|
||||
"tax_code": "064521840",
|
||||
"city_code": "19",
|
||||
"ward_code": "01936",
|
||||
"city_name": "Tỉnh Thái Nguyên",
|
||||
"ward_name": "Xã Nà Phặc",
|
||||
"is_allow_edit": true
|
||||
},
|
||||
"items": [
|
||||
{
|
||||
"name": "9crv0j6d4t",
|
||||
"item_code": "HOA E01",
|
||||
"item_name": "Hội An HOA E01",
|
||||
"description": "Hội An HOA E01",
|
||||
"qty_entered": 0.0,
|
||||
"qty_of_sm": 2.56,
|
||||
"qty_of_nos": 4.0,
|
||||
"conversion_factor": 1.5625,
|
||||
"price": 1310720.0,
|
||||
"total_amount": 3355443.2,
|
||||
"delivery_date": "2025-11-24",
|
||||
"thumbnail": "https://land.dbiz.com/files/HOA-E01-f1.jpg"
|
||||
}
|
||||
],
|
||||
"payment_terms": {
|
||||
"name": "Thanh toán hoàn toàn",
|
||||
"description": "Thanh toán ngay được chiết khấu 2%"
|
||||
},
|
||||
"timeline": [
|
||||
{
|
||||
"label": "Đã tạo đơn",
|
||||
"value": "2025-11-24 14:46:07",
|
||||
"status": "Success"
|
||||
},
|
||||
{
|
||||
"label": "Chờ phê duyệt",
|
||||
"value": null,
|
||||
"status": "Warning"
|
||||
},
|
||||
{
|
||||
"label": "Đơn đang xử lý",
|
||||
"value": "Prepare goods and transport",
|
||||
"status": "Secondary"
|
||||
},
|
||||
{
|
||||
"label": "Hoàn thành",
|
||||
"value": null,
|
||||
"status": "Secondary"
|
||||
}
|
||||
],
|
||||
"payments": [],
|
||||
"invoices": []
|
||||
}
|
||||
}
|
||||
81
docs/order_model_update_summary.md
Normal file
81
docs/order_model_update_summary.md
Normal file
@@ -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)
|
||||
@@ -12,9 +12,11 @@
|
||||
<div class="page-wrapper">
|
||||
<!-- Header -->
|
||||
<div class="header">
|
||||
<a href="checkout.html" class="back-button">
|
||||
<!--<a href="checkout.html" class="back-button">
|
||||
<i class="fas fa-arrow-left"></i>
|
||||
</a>
|
||||
</a>-->
|
||||
<div style="width: 32px;"></div>
|
||||
|
||||
<h1 class="header-title">Thanh toán</h1>
|
||||
<button class="back-button" onclick="openInfoModal()">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
|
||||
@@ -227,7 +227,25 @@ class ApiConstants {
|
||||
/// Returns: { "message": { "qr_code": "...", "amount": null, "transaction_id": "...", "bank_info": {...} } }
|
||||
static const String generateQrCode = '/building_material.building_material.api.v1.qrcode.generate';
|
||||
|
||||
/// Get user's orders
|
||||
/// Upload file (bill/invoice/attachment) (requires sid and csrf_token)
|
||||
/// POST /api/method/upload_file
|
||||
/// Form-data: { "file": File, "is_private": "1", "folder": "Home/Attachments", "doctype": "Sales Order", "docname": "SAL-ORD-2025-00058-1", "optimize": "true" }
|
||||
/// Returns: { "message": { "file_url": "...", "file_name": "...", ... } }
|
||||
static const String uploadFile = '/upload_file';
|
||||
|
||||
/// 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;
|
||||
}
|
||||
}
|
||||
@@ -59,6 +59,8 @@ class CheckoutPage extends HookConsumerWidget {
|
||||
// Price negotiation
|
||||
final needsNegotiation = useState<bool>(false);
|
||||
|
||||
final needsContract = useState(false);
|
||||
|
||||
// Watch API provider for payment terms
|
||||
final paymentTermsListAsync = ref.watch(paymentTermsListProvider);
|
||||
|
||||
@@ -240,6 +242,40 @@ class CheckoutPage extends HookConsumerWidget {
|
||||
// Price Negotiation Section
|
||||
PriceNegotiationSection(needsNegotiation: needsNegotiation),
|
||||
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
|
||||
|
||||
Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
|
||||
padding: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFFFF8E1),
|
||||
borderRadius: BorderRadius.circular(AppRadius.card),
|
||||
border: Border.all(color: const Color(0xFFFFD54F)),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Checkbox(
|
||||
value: needsContract.value,
|
||||
onChanged: (value) {
|
||||
needsContract.value = value ?? false;
|
||||
},
|
||||
activeColor: AppColors.warning,
|
||||
),
|
||||
const Expanded(
|
||||
child: Text(
|
||||
'Yêu cầu hợp đồng',
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF212121),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
/// Handles API calls for order-related data.
|
||||
library;
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:worker/core/constants/api_constants.dart';
|
||||
import 'package:worker/core/network/dio_client.dart';
|
||||
import 'package:worker/features/orders/data/models/order_status_model.dart';
|
||||
@@ -194,4 +195,128 @@ class OrderRemoteDataSource {
|
||||
throw Exception('Failed to generate QR code: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Upload bill/invoice file
|
||||
///
|
||||
/// Calls: POST /api/method/upload_file
|
||||
/// Form-data: {
|
||||
/// "file": File,
|
||||
/// "is_private": "1",
|
||||
/// "folder": "Home/Attachments",
|
||||
/// "doctype": "Sales Order",
|
||||
/// "docname": "SAL-ORD-2025-00058-1",
|
||||
/// "optimize": "true"
|
||||
/// }
|
||||
/// Returns: { "message": { "file_url": "...", "file_name": "...", ... } }
|
||||
Future<Map<String, dynamic>> uploadBill({
|
||||
required String filePath,
|
||||
required String orderId,
|
||||
}) async {
|
||||
try {
|
||||
// Create multipart form data
|
||||
final formData = FormData.fromMap({
|
||||
'file': await MultipartFile.fromFile(
|
||||
filePath,
|
||||
filename: filePath.split('/').last,
|
||||
),
|
||||
'is_private': '1',
|
||||
'folder': 'Home/Attachments',
|
||||
'doctype': 'Sales Order',
|
||||
'docname': orderId,
|
||||
'optimize': 'true',
|
||||
});
|
||||
|
||||
final response = await _dioClient.post<Map<String, dynamic>>(
|
||||
'${ApiConstants.frappeApiMethod}${ApiConstants.uploadFile}',
|
||||
data: formData,
|
||||
);
|
||||
|
||||
final data = response.data;
|
||||
if (data == null) {
|
||||
throw Exception('No data received from uploadBill API');
|
||||
}
|
||||
|
||||
// Extract file info from Frappe response
|
||||
final message = data['message'] as Map<String, dynamic>?;
|
||||
if (message == null) {
|
||||
throw Exception('No message field in uploadBill response');
|
||||
}
|
||||
|
||||
return message;
|
||||
} catch (e) {
|
||||
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"
|
||||
}
|
||||
]
|
||||
''';
|
||||
}
|
||||
502
lib/features/orders/data/models/order_detail_model.dart
Normal file
502
lib/features/orders/data/models/order_detail_model.dart
Normal file
@@ -0,0 +1,502 @@
|
||||
/// Order Detail Model
|
||||
///
|
||||
/// Data model for order detail API response.
|
||||
library;
|
||||
|
||||
import 'package:worker/features/orders/domain/entities/order_detail.dart';
|
||||
|
||||
/// Order Detail Model
|
||||
class OrderDetailModel {
|
||||
const OrderDetailModel({
|
||||
required this.order,
|
||||
required this.billingAddress,
|
||||
required this.shippingAddress,
|
||||
required this.items,
|
||||
required this.paymentTerms,
|
||||
required this.timeline,
|
||||
required this.payments,
|
||||
required this.invoices,
|
||||
});
|
||||
|
||||
final OrderDetailInfoModel order;
|
||||
final AddressInfoModel billingAddress;
|
||||
final AddressInfoModel shippingAddress;
|
||||
final List<OrderItemDetailModel> items;
|
||||
final PaymentTermsInfoModel paymentTerms;
|
||||
final List<TimelineItemModel> timeline;
|
||||
final List<dynamic> payments;
|
||||
final List<dynamic> invoices;
|
||||
|
||||
/// Create from JSON
|
||||
factory OrderDetailModel.fromJson(Map<String, dynamic> json) {
|
||||
return OrderDetailModel(
|
||||
order: OrderDetailInfoModel.fromJson(
|
||||
json['order'] as Map<String, dynamic>,
|
||||
),
|
||||
billingAddress: AddressInfoModel.fromJson(
|
||||
json['billing_address'] as Map<String, dynamic>,
|
||||
),
|
||||
shippingAddress: AddressInfoModel.fromJson(
|
||||
json['shipping_address'] as Map<String, dynamic>,
|
||||
),
|
||||
items: (json['items'] as List<dynamic>)
|
||||
.map((item) =>
|
||||
OrderItemDetailModel.fromJson(item as Map<String, dynamic>))
|
||||
.toList(),
|
||||
paymentTerms: PaymentTermsInfoModel.fromJson(
|
||||
json['payment_terms'] as Map<String, dynamic>,
|
||||
),
|
||||
timeline: (json['timeline'] as List<dynamic>)
|
||||
.map((item) =>
|
||||
TimelineItemModel.fromJson(item as Map<String, dynamic>))
|
||||
.toList(),
|
||||
payments: json['payments'] as List<dynamic>? ?? [],
|
||||
invoices: json['invoices'] as List<dynamic>? ?? [],
|
||||
);
|
||||
}
|
||||
|
||||
/// Convert to JSON
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'order': order.toJson(),
|
||||
'billing_address': billingAddress.toJson(),
|
||||
'shipping_address': shippingAddress.toJson(),
|
||||
'items': items.map((item) => item.toJson()).toList(),
|
||||
'payment_terms': paymentTerms.toJson(),
|
||||
'timeline': timeline.map((item) => item.toJson()).toList(),
|
||||
'payments': payments,
|
||||
'invoices': invoices,
|
||||
};
|
||||
}
|
||||
|
||||
/// Convert to domain entity
|
||||
OrderDetail toEntity() {
|
||||
return OrderDetail(
|
||||
order: order.toEntity(),
|
||||
billingAddress: billingAddress.toEntity(),
|
||||
shippingAddress: shippingAddress.toEntity(),
|
||||
items: items.map((item) => item.toEntity()).toList(),
|
||||
paymentTerms: paymentTerms.toEntity(),
|
||||
timeline: timeline.map((item) => item.toEntity()).toList(),
|
||||
payments: payments,
|
||||
invoices: invoices,
|
||||
);
|
||||
}
|
||||
|
||||
/// Create from domain entity
|
||||
factory OrderDetailModel.fromEntity(OrderDetail entity) {
|
||||
return OrderDetailModel(
|
||||
order: OrderDetailInfoModel.fromEntity(entity.order),
|
||||
billingAddress: AddressInfoModel.fromEntity(entity.billingAddress),
|
||||
shippingAddress: AddressInfoModel.fromEntity(entity.shippingAddress),
|
||||
items: entity.items
|
||||
.map((item) => OrderItemDetailModel.fromEntity(item))
|
||||
.toList(),
|
||||
paymentTerms: PaymentTermsInfoModel.fromEntity(entity.paymentTerms),
|
||||
timeline: entity.timeline
|
||||
.map((item) => TimelineItemModel.fromEntity(item))
|
||||
.toList(),
|
||||
payments: entity.payments,
|
||||
invoices: entity.invoices,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Order Detail Info Model
|
||||
class OrderDetailInfoModel {
|
||||
const OrderDetailInfoModel({
|
||||
required this.name,
|
||||
required this.customer,
|
||||
required this.transactionDate,
|
||||
required this.deliveryDate,
|
||||
required this.status,
|
||||
required this.statusColor,
|
||||
required this.totalQty,
|
||||
required this.total,
|
||||
required this.grandTotal,
|
||||
required this.totalRemaining,
|
||||
required this.description,
|
||||
required this.contractRequest,
|
||||
required this.ignorePricingRule,
|
||||
this.rejectionReason,
|
||||
required this.isAllowCancel,
|
||||
});
|
||||
|
||||
final String name;
|
||||
final String customer;
|
||||
final String transactionDate;
|
||||
final String deliveryDate;
|
||||
final String status;
|
||||
final String statusColor;
|
||||
final double totalQty;
|
||||
final double total;
|
||||
final double grandTotal;
|
||||
final double totalRemaining;
|
||||
final String description;
|
||||
final bool contractRequest;
|
||||
final bool ignorePricingRule;
|
||||
final String? rejectionReason;
|
||||
final bool isAllowCancel;
|
||||
|
||||
factory OrderDetailInfoModel.fromJson(Map<String, dynamic> json) {
|
||||
return OrderDetailInfoModel(
|
||||
name: json['name'] as String,
|
||||
customer: json['customer'] as String,
|
||||
transactionDate: json['transaction_date'] as String,
|
||||
deliveryDate: json['delivery_date'] as String,
|
||||
status: json['status'] as String,
|
||||
statusColor: json['status_color'] as String,
|
||||
totalQty: (json['total_qty'] as num).toDouble(),
|
||||
total: (json['total'] as num).toDouble(),
|
||||
grandTotal: (json['grand_total'] as num).toDouble(),
|
||||
totalRemaining: (json['total_remaining'] as num).toDouble(),
|
||||
description: json['description'] as String,
|
||||
contractRequest: json['contract_request'] as bool,
|
||||
ignorePricingRule: json['ignore_pricing_rule'] as bool,
|
||||
rejectionReason: json['rejection_reason'] as String?,
|
||||
isAllowCancel: json['is_allow_cancel'] as bool,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'name': name,
|
||||
'customer': customer,
|
||||
'transaction_date': transactionDate,
|
||||
'delivery_date': deliveryDate,
|
||||
'status': status,
|
||||
'status_color': statusColor,
|
||||
'total_qty': totalQty,
|
||||
'total': total,
|
||||
'grand_total': grandTotal,
|
||||
'total_remaining': totalRemaining,
|
||||
'description': description,
|
||||
'contract_request': contractRequest,
|
||||
'ignore_pricing_rule': ignorePricingRule,
|
||||
'rejection_reason': rejectionReason,
|
||||
'is_allow_cancel': isAllowCancel,
|
||||
};
|
||||
}
|
||||
|
||||
OrderDetailInfo toEntity() {
|
||||
return OrderDetailInfo(
|
||||
name: name,
|
||||
customer: customer,
|
||||
transactionDate: transactionDate,
|
||||
deliveryDate: deliveryDate,
|
||||
status: status,
|
||||
statusColor: statusColor,
|
||||
totalQty: totalQty,
|
||||
total: total,
|
||||
grandTotal: grandTotal,
|
||||
totalRemaining: totalRemaining,
|
||||
description: description,
|
||||
contractRequest: contractRequest,
|
||||
ignorePricingRule: ignorePricingRule,
|
||||
rejectionReason: rejectionReason,
|
||||
isAllowCancel: isAllowCancel,
|
||||
);
|
||||
}
|
||||
|
||||
factory OrderDetailInfoModel.fromEntity(OrderDetailInfo entity) {
|
||||
return OrderDetailInfoModel(
|
||||
name: entity.name,
|
||||
customer: entity.customer,
|
||||
transactionDate: entity.transactionDate,
|
||||
deliveryDate: entity.deliveryDate,
|
||||
status: entity.status,
|
||||
statusColor: entity.statusColor,
|
||||
totalQty: entity.totalQty,
|
||||
total: entity.total,
|
||||
grandTotal: entity.grandTotal,
|
||||
totalRemaining: entity.totalRemaining,
|
||||
description: entity.description,
|
||||
contractRequest: entity.contractRequest,
|
||||
ignorePricingRule: entity.ignorePricingRule,
|
||||
rejectionReason: entity.rejectionReason,
|
||||
isAllowCancel: entity.isAllowCancel,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Address Info Model
|
||||
class AddressInfoModel {
|
||||
const AddressInfoModel({
|
||||
required this.name,
|
||||
required this.addressTitle,
|
||||
required this.addressLine1,
|
||||
required this.phone,
|
||||
required this.email,
|
||||
this.fax,
|
||||
required this.taxCode,
|
||||
required this.cityCode,
|
||||
required this.wardCode,
|
||||
required this.cityName,
|
||||
required this.wardName,
|
||||
required this.isAllowEdit,
|
||||
});
|
||||
|
||||
final String name;
|
||||
final String addressTitle;
|
||||
final String addressLine1;
|
||||
final String phone;
|
||||
final String email;
|
||||
final String? fax;
|
||||
final String taxCode;
|
||||
final String cityCode;
|
||||
final String wardCode;
|
||||
final String cityName;
|
||||
final String wardName;
|
||||
final bool isAllowEdit;
|
||||
|
||||
factory AddressInfoModel.fromJson(Map<String, dynamic> json) {
|
||||
return AddressInfoModel(
|
||||
name: json['name'] as String,
|
||||
addressTitle: json['address_title'] as String,
|
||||
addressLine1: json['address_line1'] as String,
|
||||
phone: json['phone'] as String,
|
||||
email: json['email'] as String,
|
||||
fax: json['fax'] as String?,
|
||||
taxCode: json['tax_code'] as String,
|
||||
cityCode: json['city_code'] as String,
|
||||
wardCode: json['ward_code'] as String,
|
||||
cityName: json['city_name'] as String,
|
||||
wardName: json['ward_name'] as String,
|
||||
isAllowEdit: json['is_allow_edit'] as bool,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'name': name,
|
||||
'address_title': addressTitle,
|
||||
'address_line1': addressLine1,
|
||||
'phone': phone,
|
||||
'email': email,
|
||||
'fax': fax,
|
||||
'tax_code': taxCode,
|
||||
'city_code': cityCode,
|
||||
'ward_code': wardCode,
|
||||
'city_name': cityName,
|
||||
'ward_name': wardName,
|
||||
'is_allow_edit': isAllowEdit,
|
||||
};
|
||||
}
|
||||
|
||||
AddressInfo toEntity() {
|
||||
return AddressInfo(
|
||||
name: name,
|
||||
addressTitle: addressTitle,
|
||||
addressLine1: addressLine1,
|
||||
phone: phone,
|
||||
email: email,
|
||||
fax: fax,
|
||||
taxCode: taxCode,
|
||||
cityCode: cityCode,
|
||||
wardCode: wardCode,
|
||||
cityName: cityName,
|
||||
wardName: wardName,
|
||||
isAllowEdit: isAllowEdit,
|
||||
);
|
||||
}
|
||||
|
||||
factory AddressInfoModel.fromEntity(AddressInfo entity) {
|
||||
return AddressInfoModel(
|
||||
name: entity.name,
|
||||
addressTitle: entity.addressTitle,
|
||||
addressLine1: entity.addressLine1,
|
||||
phone: entity.phone,
|
||||
email: entity.email,
|
||||
fax: entity.fax,
|
||||
taxCode: entity.taxCode,
|
||||
cityCode: entity.cityCode,
|
||||
wardCode: entity.wardCode,
|
||||
cityName: entity.cityName,
|
||||
wardName: entity.wardName,
|
||||
isAllowEdit: entity.isAllowEdit,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Order Item Detail Model
|
||||
class OrderItemDetailModel {
|
||||
const OrderItemDetailModel({
|
||||
required this.name,
|
||||
required this.itemCode,
|
||||
required this.itemName,
|
||||
required this.description,
|
||||
required this.qtyEntered,
|
||||
required this.qtyOfSm,
|
||||
required this.qtyOfNos,
|
||||
required this.conversionFactor,
|
||||
required this.price,
|
||||
required this.totalAmount,
|
||||
required this.deliveryDate,
|
||||
this.thumbnail,
|
||||
});
|
||||
|
||||
final String name;
|
||||
final String itemCode;
|
||||
final String itemName;
|
||||
final String description;
|
||||
final double qtyEntered;
|
||||
final double qtyOfSm;
|
||||
final double qtyOfNos;
|
||||
final double conversionFactor;
|
||||
final double price;
|
||||
final double totalAmount;
|
||||
final String deliveryDate;
|
||||
final String? thumbnail;
|
||||
|
||||
factory OrderItemDetailModel.fromJson(Map<String, dynamic> json) {
|
||||
return OrderItemDetailModel(
|
||||
name: json['name'] as String,
|
||||
itemCode: json['item_code'] as String,
|
||||
itemName: json['item_name'] as String,
|
||||
description: json['description'] as String,
|
||||
qtyEntered: (json['qty_entered'] as num).toDouble(),
|
||||
qtyOfSm: (json['qty_of_sm'] as num).toDouble(),
|
||||
qtyOfNos: (json['qty_of_nos'] as num).toDouble(),
|
||||
conversionFactor: (json['conversion_factor'] as num).toDouble(),
|
||||
price: (json['price'] as num).toDouble(),
|
||||
totalAmount: (json['total_amount'] as num).toDouble(),
|
||||
deliveryDate: json['delivery_date'] as String,
|
||||
thumbnail: json['thumbnail'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'name': name,
|
||||
'item_code': itemCode,
|
||||
'item_name': itemName,
|
||||
'description': description,
|
||||
'qty_entered': qtyEntered,
|
||||
'qty_of_sm': qtyOfSm,
|
||||
'qty_of_nos': qtyOfNos,
|
||||
'conversion_factor': conversionFactor,
|
||||
'price': price,
|
||||
'total_amount': totalAmount,
|
||||
'delivery_date': deliveryDate,
|
||||
'thumbnail': thumbnail,
|
||||
};
|
||||
}
|
||||
|
||||
OrderItemDetail toEntity() {
|
||||
return OrderItemDetail(
|
||||
name: name,
|
||||
itemCode: itemCode,
|
||||
itemName: itemName,
|
||||
description: description,
|
||||
qtyEntered: qtyEntered,
|
||||
qtyOfSm: qtyOfSm,
|
||||
qtyOfNos: qtyOfNos,
|
||||
conversionFactor: conversionFactor,
|
||||
price: price,
|
||||
totalAmount: totalAmount,
|
||||
deliveryDate: deliveryDate,
|
||||
thumbnail: thumbnail,
|
||||
);
|
||||
}
|
||||
|
||||
factory OrderItemDetailModel.fromEntity(OrderItemDetail entity) {
|
||||
return OrderItemDetailModel(
|
||||
name: entity.name,
|
||||
itemCode: entity.itemCode,
|
||||
itemName: entity.itemName,
|
||||
description: entity.description,
|
||||
qtyEntered: entity.qtyEntered,
|
||||
qtyOfSm: entity.qtyOfSm,
|
||||
qtyOfNos: entity.qtyOfNos,
|
||||
conversionFactor: entity.conversionFactor,
|
||||
price: entity.price,
|
||||
totalAmount: entity.totalAmount,
|
||||
deliveryDate: entity.deliveryDate,
|
||||
thumbnail: entity.thumbnail,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Payment Terms Info Model
|
||||
class PaymentTermsInfoModel {
|
||||
const PaymentTermsInfoModel({
|
||||
required this.name,
|
||||
required this.description,
|
||||
});
|
||||
|
||||
final String name;
|
||||
final String description;
|
||||
|
||||
factory PaymentTermsInfoModel.fromJson(Map<String, dynamic> json) {
|
||||
return PaymentTermsInfoModel(
|
||||
name: json['name'] as String,
|
||||
description: json['description'] as String,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'name': name,
|
||||
'description': description,
|
||||
};
|
||||
}
|
||||
|
||||
PaymentTermsInfo toEntity() {
|
||||
return PaymentTermsInfo(
|
||||
name: name,
|
||||
description: description,
|
||||
);
|
||||
}
|
||||
|
||||
factory PaymentTermsInfoModel.fromEntity(PaymentTermsInfo entity) {
|
||||
return PaymentTermsInfoModel(
|
||||
name: entity.name,
|
||||
description: entity.description,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Timeline Item Model
|
||||
class TimelineItemModel {
|
||||
const TimelineItemModel({
|
||||
required this.label,
|
||||
this.value,
|
||||
required this.status,
|
||||
});
|
||||
|
||||
final String label;
|
||||
final String? value;
|
||||
final String status;
|
||||
|
||||
factory TimelineItemModel.fromJson(Map<String, dynamic> json) {
|
||||
return TimelineItemModel(
|
||||
label: json['label'] as String,
|
||||
value: json['value'] as String?,
|
||||
status: json['status'] as String,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'label': label,
|
||||
'value': value,
|
||||
'status': status,
|
||||
};
|
||||
}
|
||||
|
||||
TimelineItem toEntity() {
|
||||
return TimelineItem(
|
||||
label: label,
|
||||
value: value,
|
||||
status: status,
|
||||
);
|
||||
}
|
||||
|
||||
factory TimelineItemModel.fromEntity(TimelineItem entity) {
|
||||
return TimelineItemModel(
|
||||
label: entity.label,
|
||||
value: entity.value,
|
||||
status: entity.status,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,80 @@
|
||||
library;
|
||||
|
||||
import 'package:worker/features/orders/data/datasources/order_remote_datasource.dart';
|
||||
import 'package:worker/features/orders/data/datasources/order_status_local_datasource.dart';
|
||||
import 'package:worker/features/orders/data/models/order_detail_model.dart';
|
||||
import 'package:worker/features/orders/data/models/order_model.dart';
|
||||
import 'package:worker/features/orders/domain/entities/order.dart';
|
||||
import 'package:worker/features/orders/domain/entities/order_detail.dart';
|
||||
import 'package:worker/features/orders/domain/entities/order_status.dart';
|
||||
import 'package:worker/features/orders/domain/entities/payment_term.dart';
|
||||
import 'package:worker/features/orders/domain/repositories/order_repository.dart';
|
||||
|
||||
/// 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<OrderDetail> getOrderDetail(String orderId) async {
|
||||
try {
|
||||
final detailData = await _remoteDataSource.getOrderDetail(orderId);
|
||||
// Convert JSON → Model → Entity
|
||||
return OrderDetailModel.fromJson(detailData).toEntity();
|
||||
} catch (e) {
|
||||
throw Exception('Failed to get order detail: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<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');
|
||||
}
|
||||
}
|
||||
@@ -65,4 +123,19 @@ class OrderRepositoryImpl implements OrderRepository {
|
||||
throw Exception('Failed to generate QR code: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Map<String, dynamic>> uploadBill({
|
||||
required String filePath,
|
||||
required String orderId,
|
||||
}) async {
|
||||
try {
|
||||
return await _remoteDataSource.uploadBill(
|
||||
filePath: filePath,
|
||||
orderId: orderId,
|
||||
);
|
||||
} catch (e) {
|
||||
throw Exception('Failed to upload bill: $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,
|
||||
List<Object?> get props => [
|
||||
name,
|
||||
transactionDate,
|
||||
deliveryDate,
|
||||
address,
|
||||
grandTotal,
|
||||
status,
|
||||
totalAmount,
|
||||
discountAmount,
|
||||
taxAmount,
|
||||
shippingFee,
|
||||
finalAmount,
|
||||
);
|
||||
}
|
||||
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)';
|
||||
}
|
||||
}
|
||||
|
||||
221
lib/features/orders/domain/entities/order_detail.dart
Normal file
221
lib/features/orders/domain/entities/order_detail.dart
Normal file
@@ -0,0 +1,221 @@
|
||||
/// Order Detail Entity
|
||||
///
|
||||
/// Complete order detail information including addresses, items, timeline, etc.
|
||||
library;
|
||||
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
/// Order Detail Entity
|
||||
class OrderDetail extends Equatable {
|
||||
const OrderDetail({
|
||||
required this.order,
|
||||
required this.billingAddress,
|
||||
required this.shippingAddress,
|
||||
required this.items,
|
||||
required this.paymentTerms,
|
||||
required this.timeline,
|
||||
required this.payments,
|
||||
required this.invoices,
|
||||
});
|
||||
|
||||
final OrderDetailInfo order;
|
||||
final AddressInfo billingAddress;
|
||||
final AddressInfo shippingAddress;
|
||||
final List<OrderItemDetail> items;
|
||||
final PaymentTermsInfo paymentTerms;
|
||||
final List<TimelineItem> timeline;
|
||||
final List<dynamic> payments; // Payment entities can be added later
|
||||
final List<dynamic> invoices; // Invoice entities can be added later
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
order,
|
||||
billingAddress,
|
||||
shippingAddress,
|
||||
items,
|
||||
paymentTerms,
|
||||
timeline,
|
||||
payments,
|
||||
invoices,
|
||||
];
|
||||
}
|
||||
|
||||
/// Order Detail Info
|
||||
class OrderDetailInfo extends Equatable {
|
||||
const OrderDetailInfo({
|
||||
required this.name,
|
||||
required this.customer,
|
||||
required this.transactionDate,
|
||||
required this.deliveryDate,
|
||||
required this.status,
|
||||
required this.statusColor,
|
||||
required this.totalQty,
|
||||
required this.total,
|
||||
required this.grandTotal,
|
||||
required this.totalRemaining,
|
||||
required this.description,
|
||||
required this.contractRequest,
|
||||
required this.ignorePricingRule,
|
||||
this.rejectionReason,
|
||||
required this.isAllowCancel,
|
||||
});
|
||||
|
||||
final String name;
|
||||
final String customer;
|
||||
final String transactionDate;
|
||||
final String deliveryDate;
|
||||
final String status;
|
||||
final String statusColor;
|
||||
final double totalQty;
|
||||
final double total;
|
||||
final double grandTotal;
|
||||
final double totalRemaining;
|
||||
final String description;
|
||||
final bool contractRequest;
|
||||
final bool ignorePricingRule;
|
||||
final String? rejectionReason;
|
||||
final bool isAllowCancel;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
name,
|
||||
customer,
|
||||
transactionDate,
|
||||
deliveryDate,
|
||||
status,
|
||||
statusColor,
|
||||
totalQty,
|
||||
total,
|
||||
grandTotal,
|
||||
totalRemaining,
|
||||
description,
|
||||
contractRequest,
|
||||
ignorePricingRule,
|
||||
rejectionReason,
|
||||
isAllowCancel,
|
||||
];
|
||||
}
|
||||
|
||||
/// Address Info
|
||||
class AddressInfo extends Equatable {
|
||||
const AddressInfo({
|
||||
required this.name,
|
||||
required this.addressTitle,
|
||||
required this.addressLine1,
|
||||
required this.phone,
|
||||
required this.email,
|
||||
this.fax,
|
||||
required this.taxCode,
|
||||
required this.cityCode,
|
||||
required this.wardCode,
|
||||
required this.cityName,
|
||||
required this.wardName,
|
||||
required this.isAllowEdit,
|
||||
});
|
||||
|
||||
final String name;
|
||||
final String addressTitle;
|
||||
final String addressLine1;
|
||||
final String phone;
|
||||
final String email;
|
||||
final String? fax;
|
||||
final String taxCode;
|
||||
final String cityCode;
|
||||
final String wardCode;
|
||||
final String cityName;
|
||||
final String wardName;
|
||||
final bool isAllowEdit;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
name,
|
||||
addressTitle,
|
||||
addressLine1,
|
||||
phone,
|
||||
email,
|
||||
fax,
|
||||
taxCode,
|
||||
cityCode,
|
||||
wardCode,
|
||||
cityName,
|
||||
wardName,
|
||||
isAllowEdit,
|
||||
];
|
||||
}
|
||||
|
||||
/// Order Item Detail
|
||||
class OrderItemDetail extends Equatable {
|
||||
const OrderItemDetail({
|
||||
required this.name,
|
||||
required this.itemCode,
|
||||
required this.itemName,
|
||||
required this.description,
|
||||
required this.qtyEntered,
|
||||
required this.qtyOfSm,
|
||||
required this.qtyOfNos,
|
||||
required this.conversionFactor,
|
||||
required this.price,
|
||||
required this.totalAmount,
|
||||
required this.deliveryDate,
|
||||
this.thumbnail,
|
||||
});
|
||||
|
||||
final String name;
|
||||
final String itemCode;
|
||||
final String itemName;
|
||||
final String description;
|
||||
final double qtyEntered;
|
||||
final double qtyOfSm;
|
||||
final double qtyOfNos;
|
||||
final double conversionFactor;
|
||||
final double price;
|
||||
final double totalAmount;
|
||||
final String deliveryDate;
|
||||
final String? thumbnail;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
name,
|
||||
itemCode,
|
||||
itemName,
|
||||
description,
|
||||
qtyEntered,
|
||||
qtyOfSm,
|
||||
qtyOfNos,
|
||||
conversionFactor,
|
||||
price,
|
||||
totalAmount,
|
||||
deliveryDate,
|
||||
thumbnail,
|
||||
];
|
||||
}
|
||||
|
||||
/// Payment Terms Info
|
||||
class PaymentTermsInfo extends Equatable {
|
||||
const PaymentTermsInfo({
|
||||
required this.name,
|
||||
required this.description,
|
||||
});
|
||||
|
||||
final String name;
|
||||
final String description;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [name, description];
|
||||
}
|
||||
|
||||
/// Timeline Item
|
||||
class TimelineItem extends Equatable {
|
||||
const TimelineItem({
|
||||
required this.label,
|
||||
this.value,
|
||||
required this.status,
|
||||
});
|
||||
|
||||
final String label;
|
||||
final String? value;
|
||||
final String status; // Success, Warning, Secondary, etc.
|
||||
|
||||
@override
|
||||
List<Object?> get props => [label, value, status];
|
||||
}
|
||||
@@ -3,11 +3,22 @@
|
||||
/// 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_detail.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 order detail by ID
|
||||
Future<OrderDetail> getOrderDetail(String orderId);
|
||||
|
||||
/// Get list of available order statuses
|
||||
Future<List<OrderStatus>> getOrderStatusList();
|
||||
|
||||
@@ -26,4 +37,10 @@ abstract class OrderRepository {
|
||||
|
||||
/// Generate QR code for payment
|
||||
Future<Map<String, dynamic>> generateQrCode(String orderId);
|
||||
|
||||
/// Upload bill/invoice file
|
||||
Future<Map<String, dynamic>> uploadBill({
|
||||
required String filePath,
|
||||
required String orderId,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,14 +3,17 @@
|
||||
/// Displays detailed order information including status timeline, delivery info, and products.
|
||||
library;
|
||||
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:worker/core/constants/ui_constants.dart';
|
||||
import 'package:worker/core/database/models/enums.dart';
|
||||
import 'package:worker/core/enums/status_color.dart';
|
||||
import 'package:worker/core/theme/colors.dart';
|
||||
import 'package:worker/features/orders/domain/entities/order_detail.dart';
|
||||
import 'package:worker/features/orders/presentation/providers/orders_provider.dart';
|
||||
|
||||
/// Order Detail Page
|
||||
///
|
||||
@@ -27,9 +30,7 @@ class OrderDetailPage extends ConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
// TODO: Replace with actual order data from provider
|
||||
// For now using mock data based on HTML reference
|
||||
final mockOrder = _getMockOrder();
|
||||
final orderDetailAsync = ref.watch(orderDetailProvider(orderId));
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFF4F6F8),
|
||||
@@ -69,49 +70,28 @@ class OrderDetailPage extends ConsumerWidget {
|
||||
foregroundColor: AppColors.grey900,
|
||||
centerTitle: false,
|
||||
),
|
||||
body: Stack(
|
||||
body: orderDetailAsync.when(
|
||||
data: (orderDetail) {
|
||||
return Stack(
|
||||
children: [
|
||||
SingleChildScrollView(
|
||||
padding: const EdgeInsets.only(bottom: 100),
|
||||
child: Column(
|
||||
children: [
|
||||
// Status Timeline Card
|
||||
_buildStatusTimelineCard(
|
||||
mockOrder['orderNumber']! as String,
|
||||
mockOrder['status']! as OrderStatus,
|
||||
mockOrder['statusHistory']! as List<Map<String, dynamic>>,
|
||||
),
|
||||
_buildStatusTimelineCard(orderDetail),
|
||||
|
||||
// Delivery Information Card
|
||||
_buildDeliveryInfoCard(
|
||||
mockOrder['deliveryMethod']! as String,
|
||||
mockOrder['warehouseDate']! as DateTime,
|
||||
mockOrder['deliveryDate']! as DateTime,
|
||||
mockOrder['deliveryAddress']! as String,
|
||||
mockOrder['receiverName']! as String,
|
||||
mockOrder['receiverPhone']! as String,
|
||||
),
|
||||
// Delivery/Address Information Card
|
||||
_buildAddressInfoCard(orderDetail),
|
||||
|
||||
// Customer Information Card
|
||||
_buildCustomerInfoCard(
|
||||
mockOrder['customerName']! as String,
|
||||
mockOrder['customerPhone']! as String,
|
||||
mockOrder['customerEmail']! as String,
|
||||
mockOrder['customerType']! as String,
|
||||
),
|
||||
_buildCustomerInfoCard(orderDetail),
|
||||
|
||||
// Products List Card
|
||||
_buildProductsListCard(),
|
||||
_buildProductsListCard(orderDetail),
|
||||
|
||||
// Order Summary Card
|
||||
_buildOrderSummaryCard(
|
||||
mockOrder['subtotal']! as double,
|
||||
mockOrder['shippingFee']! as double,
|
||||
mockOrder['discount']! as double,
|
||||
mockOrder['total']! as double,
|
||||
mockOrder['paymentMethod']! as String,
|
||||
mockOrder['notes'] as String?,
|
||||
),
|
||||
_buildOrderSummaryCard(orderDetail),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -121,84 +101,67 @@ class OrderDetailPage extends ConsumerWidget {
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.white,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.1),
|
||||
blurRadius: 15,
|
||||
offset: const Offset(0, -4),
|
||||
child: _buildActionButtons(context, orderDetail),
|
||||
),
|
||||
],
|
||||
),
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
spacing: 12,
|
||||
);
|
||||
},
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error: (error, stack) => Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Expanded(
|
||||
child: OutlinedButton.icon(
|
||||
const FaIcon(
|
||||
FontAwesomeIcons.circleExclamation,
|
||||
size: 64,
|
||||
color: AppColors.danger,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
'Không thể tải thông tin đơn hàng',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.grey900,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
error.toString(),
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: AppColors.grey500,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
// TODO: Implement contact customer
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Gọi điện cho khách hàng...'),
|
||||
),
|
||||
);
|
||||
ref.invalidate(orderDetailProvider(orderId));
|
||||
},
|
||||
icon: const FaIcon(FontAwesomeIcons.phone, size: 18),
|
||||
label: const Text('Liên hệ khách hàng'),
|
||||
style: OutlinedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
side: const BorderSide(
|
||||
color: AppColors.grey100,
|
||||
width: 2,
|
||||
),
|
||||
foregroundColor: AppColors.grey900,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
// TODO: Implement update status
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Cập nhật trạng thái...'),
|
||||
),
|
||||
);
|
||||
},
|
||||
icon: const FaIcon(FontAwesomeIcons.penToSquare, size: 18),
|
||||
label: const Text('Cập nhật trạng thái'),
|
||||
icon: const FaIcon(FontAwesomeIcons.arrowsRotate, size: 16),
|
||||
label: const Text('Thử lại'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
backgroundColor: AppColors.primaryBlue,
|
||||
foregroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 24,
|
||||
vertical: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Build Status Timeline Card
|
||||
Widget _buildStatusTimelineCard(
|
||||
String orderNumber,
|
||||
OrderStatus currentStatus,
|
||||
List<Map<String, dynamic>> statusHistory,
|
||||
) {
|
||||
Widget _buildStatusTimelineCard(OrderDetail orderDetail) {
|
||||
final order = orderDetail.order;
|
||||
final timeline = orderDetail.timeline;
|
||||
|
||||
return Card(
|
||||
margin: const EdgeInsets.all(16),
|
||||
elevation: 1,
|
||||
@@ -209,33 +172,30 @@ class OrderDetailPage extends ConsumerWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Order Number and Status Badge
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'#$orderNumber',
|
||||
'#${order.name}',
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColors.primaryBlue,
|
||||
),
|
||||
),
|
||||
_buildStatusBadge(currentStatus),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 12,),
|
||||
_buildStatusBadge(order.status, order.statusColor),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Status Timeline
|
||||
...statusHistory.asMap().entries.map((entry) {
|
||||
...timeline.asMap().entries.map((entry) {
|
||||
final index = entry.key;
|
||||
final item = entry.value;
|
||||
final isLast = index == statusHistory.length - 1;
|
||||
final isLast = index == timeline.length - 1;
|
||||
|
||||
return _buildTimelineItem(
|
||||
title: item['title']! as String,
|
||||
date: item['date'] as String?,
|
||||
status: item['status']! as String,
|
||||
title: item.label,
|
||||
date: item.value,
|
||||
status: item.status,
|
||||
isLast: isLast,
|
||||
);
|
||||
}),
|
||||
@@ -249,25 +209,25 @@ class OrderDetailPage extends ConsumerWidget {
|
||||
Widget _buildTimelineItem({
|
||||
required String title,
|
||||
String? date,
|
||||
required String status, // 'completed', 'active', 'pending'
|
||||
required String status, // 'Success', 'Warning', 'Secondary', etc.
|
||||
required bool isLast,
|
||||
}) {
|
||||
final statusColor = StatusColor.fromString(status) ?? StatusColor.secondary;
|
||||
|
||||
Color iconColor;
|
||||
Color iconBgColor;
|
||||
IconData iconData;
|
||||
|
||||
switch (status) {
|
||||
case 'completed':
|
||||
if (statusColor == StatusColor.success) {
|
||||
iconColor = Colors.white;
|
||||
iconBgColor = AppColors.success;
|
||||
iconBgColor = statusColor.color;
|
||||
iconData = FontAwesomeIcons.check;
|
||||
break;
|
||||
case 'active':
|
||||
} else if (statusColor == StatusColor.warning) {
|
||||
iconColor = Colors.white;
|
||||
iconBgColor = AppColors.warning;
|
||||
iconBgColor = statusColor.color;
|
||||
iconData = FontAwesomeIcons.gear;
|
||||
break;
|
||||
default: // pending
|
||||
} else {
|
||||
// Secondary or other
|
||||
iconColor = AppColors.grey500;
|
||||
iconBgColor = AppColors.grey100;
|
||||
iconData = _getIconForTitle(title);
|
||||
@@ -286,7 +246,7 @@ class OrderDetailPage extends ConsumerWidget {
|
||||
color: iconBgColor,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: FaIcon(iconData, size: 10, color: iconColor),
|
||||
child: Center(child: FaIcon(iconData, size: 10, color: iconColor)),
|
||||
),
|
||||
if (!isLast)
|
||||
Container(
|
||||
@@ -342,85 +302,34 @@ class OrderDetailPage extends ConsumerWidget {
|
||||
}
|
||||
|
||||
/// Build Status Badge
|
||||
Widget _buildStatusBadge(OrderStatus status) {
|
||||
Widget _buildStatusBadge(String status, String statusColorName) {
|
||||
final statusColor = StatusColor.fromString(statusColorName) ?? StatusColor.secondary;
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: _getStatusColor(status).withValues(alpha: 0.1),
|
||||
color: statusColor.light,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(
|
||||
color: _getStatusColor(status).withValues(alpha: 0.3),
|
||||
color: statusColor.border,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
_getStatusText(status),
|
||||
status,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: _getStatusColor(status),
|
||||
color: statusColor.color,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Get status color
|
||||
Color _getStatusColor(OrderStatus status) {
|
||||
switch (status) {
|
||||
case OrderStatus.draft:
|
||||
return AppColors.grey500;
|
||||
case OrderStatus.pending:
|
||||
return AppColors.info;
|
||||
case OrderStatus.confirmed:
|
||||
return AppColors.info;
|
||||
case OrderStatus.processing:
|
||||
return AppColors.warning;
|
||||
case OrderStatus.shipped:
|
||||
return AppColors.primaryBlue;
|
||||
case OrderStatus.delivered:
|
||||
return AppColors.success;
|
||||
case OrderStatus.completed:
|
||||
return AppColors.success;
|
||||
case OrderStatus.cancelled:
|
||||
return AppColors.danger;
|
||||
case OrderStatus.refunded:
|
||||
return const Color(0xFFF97316);
|
||||
}
|
||||
}
|
||||
|
||||
/// Get status text
|
||||
String _getStatusText(OrderStatus status) {
|
||||
switch (status) {
|
||||
case OrderStatus.draft:
|
||||
return 'Nháp';
|
||||
case OrderStatus.pending:
|
||||
return 'Chờ xác nhận';
|
||||
case OrderStatus.confirmed:
|
||||
return 'Đã xác nhận';
|
||||
case OrderStatus.processing:
|
||||
return 'Đang xử lý';
|
||||
case OrderStatus.shipped:
|
||||
return 'Đang vận chuyển';
|
||||
case OrderStatus.delivered:
|
||||
return 'Đã giao hàng';
|
||||
case OrderStatus.completed:
|
||||
return 'Hoàn thành';
|
||||
case OrderStatus.cancelled:
|
||||
return 'Đã hủy';
|
||||
case OrderStatus.refunded:
|
||||
return 'Đã hoàn tiền';
|
||||
}
|
||||
}
|
||||
|
||||
/// Build Delivery Info Card
|
||||
Widget _buildDeliveryInfoCard(
|
||||
String deliveryMethod,
|
||||
DateTime warehouseDate,
|
||||
DateTime deliveryDate,
|
||||
String deliveryAddress,
|
||||
String receiverName,
|
||||
String receiverPhone,
|
||||
) {
|
||||
/// Build Address Info Card
|
||||
Widget _buildAddressInfoCard(OrderDetail orderDetail) {
|
||||
final order = orderDetail.order;
|
||||
final shippingAddress = orderDetail.shippingAddress;
|
||||
final dateFormatter = DateFormat('dd/MM/yyyy');
|
||||
|
||||
return Card(
|
||||
@@ -453,72 +362,11 @@ class OrderDetailPage extends ConsumerWidget {
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Delivery Method
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.grey50,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.primaryBlue,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const FaIcon(
|
||||
FontAwesomeIcons.truck,
|
||||
color: Colors.white,
|
||||
size: 18,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
deliveryMethod,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.grey900,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
const Text(
|
||||
'Giao trong 3-5 ngày làm việc',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppColors.grey500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Delivery Details
|
||||
// Delivery Date
|
||||
_buildInfoRow(
|
||||
icon: FontAwesomeIcons.calendar,
|
||||
label: 'Ngày xuất kho',
|
||||
value: dateFormatter.format(warehouseDate),
|
||||
valueColor: AppColors.success,
|
||||
),
|
||||
|
||||
const SizedBox(height: 12),
|
||||
|
||||
_buildInfoRow(
|
||||
icon: FontAwesomeIcons.clock,
|
||||
label: 'Thời gian giao hàng',
|
||||
value: '${dateFormatter.format(deliveryDate)}, 8:00 - 17:00',
|
||||
label: 'Ngày giao hàng',
|
||||
value: dateFormatter.format(DateTime.parse(order.deliveryDate)),
|
||||
),
|
||||
|
||||
const SizedBox(height: 12),
|
||||
@@ -526,7 +374,8 @@ class OrderDetailPage extends ConsumerWidget {
|
||||
_buildInfoRow(
|
||||
icon: FontAwesomeIcons.locationDot,
|
||||
label: 'Địa chỉ giao hàng',
|
||||
value: deliveryAddress,
|
||||
value:
|
||||
'${shippingAddress.addressLine1}\n${shippingAddress.wardName}, ${shippingAddress.cityName}',
|
||||
),
|
||||
|
||||
const SizedBox(height: 12),
|
||||
@@ -534,7 +383,7 @@ class OrderDetailPage extends ConsumerWidget {
|
||||
_buildInfoRow(
|
||||
icon: FontAwesomeIcons.user,
|
||||
label: 'Người nhận',
|
||||
value: '$receiverName - $receiverPhone',
|
||||
value: '${shippingAddress.addressTitle} - ${shippingAddress.phone}',
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -590,12 +439,9 @@ class OrderDetailPage extends ConsumerWidget {
|
||||
}
|
||||
|
||||
/// Build Customer Info Card
|
||||
Widget _buildCustomerInfoCard(
|
||||
String customerName,
|
||||
String customerPhone,
|
||||
String customerEmail,
|
||||
String customerType,
|
||||
) {
|
||||
Widget _buildCustomerInfoCard(OrderDetail orderDetail) {
|
||||
final order = orderDetail.order;
|
||||
final billingAddress = orderDetail.billingAddress;
|
||||
return Card(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
elevation: 1,
|
||||
@@ -626,46 +472,19 @@ class OrderDetailPage extends ConsumerWidget {
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
_buildCustomerRow('Tên khách hàng:', customerName),
|
||||
_buildCustomerRow('Tên khách hàng:', order.customer),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
_buildCustomerRow('Số điện thoại:', customerPhone),
|
||||
_buildCustomerRow('Số điện thoại:', billingAddress.phone),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
_buildCustomerRow('Email:', customerEmail),
|
||||
_buildCustomerRow('Email:', billingAddress.email),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text(
|
||||
'Loại khách hàng:',
|
||||
style: TextStyle(fontSize: 14, color: AppColors.grey500),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
colors: [Color(0xFFFFD700), Color(0xFFFFA500)],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Text(
|
||||
customerType,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (billingAddress.taxCode.isNotEmpty) ...[
|
||||
_buildCustomerRow('Mã số thuế:', billingAddress.taxCode),
|
||||
const SizedBox(height: 12),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -694,25 +513,13 @@ class OrderDetailPage extends ConsumerWidget {
|
||||
}
|
||||
|
||||
/// Build Products List Card
|
||||
Widget _buildProductsListCard() {
|
||||
final products = [
|
||||
{
|
||||
'name': 'Gạch Eurotile MỘC LAM E03',
|
||||
'size': '60x60cm',
|
||||
'sku': 'ET-ML-E03-60x60',
|
||||
'quantity': '30 m²',
|
||||
'unitPrice': '285.000đ/m²',
|
||||
'totalPrice': '8.550.000đ',
|
||||
},
|
||||
{
|
||||
'name': 'Gạch Eurotile STONE GREY S02',
|
||||
'size': '80x80cm',
|
||||
'sku': 'ET-SG-S02-80x80',
|
||||
'quantity': '20 m²',
|
||||
'unitPrice': '217.500đ/m²',
|
||||
'totalPrice': '4.350.000đ',
|
||||
},
|
||||
];
|
||||
Widget _buildProductsListCard(OrderDetail orderDetail) {
|
||||
final items = orderDetail.items;
|
||||
final currencyFormatter = NumberFormat.currency(
|
||||
locale: 'vi_VN',
|
||||
symbol: 'đ',
|
||||
decimalDigits: 0,
|
||||
);
|
||||
|
||||
return Card(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
@@ -744,8 +551,8 @@ class OrderDetailPage extends ConsumerWidget {
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
...products.map(
|
||||
(product) => Container(
|
||||
...items.map(
|
||||
(item) => Container(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
@@ -756,6 +563,35 @@ class OrderDetailPage extends ConsumerWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Product Image
|
||||
if (item.thumbnail != null)
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: item.thumbnail!,
|
||||
width: 60,
|
||||
height: 60,
|
||||
fit: BoxFit.cover,
|
||||
placeholder: (context, url) => Container(
|
||||
width: 60,
|
||||
height: 60,
|
||||
color: AppColors.grey50,
|
||||
child: const Center(
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
),
|
||||
),
|
||||
errorWidget: (context, url, error) => Container(
|
||||
width: 60,
|
||||
height: 60,
|
||||
color: AppColors.grey50,
|
||||
child: const FaIcon(
|
||||
FontAwesomeIcons.image,
|
||||
color: AppColors.grey500,
|
||||
size: 28,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
Container(
|
||||
width: 60,
|
||||
height: 60,
|
||||
@@ -778,7 +614,7 @@ class OrderDetailPage extends ConsumerWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
product['name']!,
|
||||
item.itemName,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
@@ -787,14 +623,7 @@ class OrderDetailPage extends ConsumerWidget {
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'Kích thước: ${product['size']}',
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppColors.grey500,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'SKU: ${product['sku']}',
|
||||
'Mã: ${item.itemCode}',
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppColors.grey500,
|
||||
@@ -815,7 +644,7 @@ class OrderDetailPage extends ConsumerWidget {
|
||||
),
|
||||
),
|
||||
Text(
|
||||
product['quantity']!,
|
||||
'${item.qtyOfSm} m²',
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
@@ -828,14 +657,14 @@ class OrderDetailPage extends ConsumerWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
product['unitPrice']!,
|
||||
'${currencyFormatter.format(item.price)}/m²',
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppColors.grey500,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
product['totalPrice']!,
|
||||
currencyFormatter.format(item.totalAmount),
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
@@ -860,14 +689,9 @@ class OrderDetailPage extends ConsumerWidget {
|
||||
}
|
||||
|
||||
/// Build Order Summary Card
|
||||
Widget _buildOrderSummaryCard(
|
||||
double subtotal,
|
||||
double shippingFee,
|
||||
double discount,
|
||||
double total,
|
||||
String paymentMethod,
|
||||
String? notes,
|
||||
) {
|
||||
Widget _buildOrderSummaryCard(OrderDetail orderDetail) {
|
||||
final order = orderDetail.order;
|
||||
final paymentTerms = orderDetail.paymentTerms;
|
||||
final currencyFormatter = NumberFormat.currency(
|
||||
locale: 'vi_VN',
|
||||
symbol: 'đ',
|
||||
@@ -904,35 +728,29 @@ class OrderDetailPage extends ConsumerWidget {
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
_buildSummaryRow('Tạm tính:', currencyFormatter.format(subtotal)),
|
||||
_buildSummaryRow('Tổng tiền hàng:', currencyFormatter.format(order.total)),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
if (order.totalRemaining > 0) ...[
|
||||
_buildSummaryRow(
|
||||
'Phí vận chuyển:',
|
||||
shippingFee == 0
|
||||
? 'Miễn phí'
|
||||
: currencyFormatter.format(shippingFee),
|
||||
valueColor: shippingFee == 0 ? AppColors.success : null,
|
||||
'Còn lại:',
|
||||
currencyFormatter.format(order.totalRemaining),
|
||||
valueColor: AppColors.warning,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
_buildSummaryRow(
|
||||
'Giảm giá VIP:',
|
||||
'-${currencyFormatter.format(discount)}',
|
||||
valueColor: AppColors.success,
|
||||
),
|
||||
],
|
||||
|
||||
const Divider(height: 24),
|
||||
|
||||
_buildSummaryRow(
|
||||
'Tổng cộng:',
|
||||
currencyFormatter.format(total),
|
||||
currencyFormatter.format(order.grandTotal),
|
||||
isTotal: true,
|
||||
),
|
||||
|
||||
const Divider(height: 24),
|
||||
|
||||
// Payment Method
|
||||
// Payment Terms
|
||||
Row(
|
||||
children: [
|
||||
const FaIcon(
|
||||
@@ -942,22 +760,31 @@ class OrderDetailPage extends ConsumerWidget {
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
const Text(
|
||||
'Phương thức thanh toán:',
|
||||
'Điều khoản thanh toán:',
|
||||
style: TextStyle(fontSize: 14, color: AppColors.grey500),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
paymentMethod,
|
||||
paymentTerms.name,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.grey900,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
paymentTerms.description,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppColors.grey500,
|
||||
height: 1.4,
|
||||
),
|
||||
),
|
||||
|
||||
if (notes != null) ...[
|
||||
if (order.description.isNotEmpty) ...[
|
||||
const Divider(height: 24),
|
||||
|
||||
// Order Notes
|
||||
@@ -973,7 +800,7 @@ class OrderDetailPage extends ConsumerWidget {
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
notes,
|
||||
order.description,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
@@ -1019,56 +846,73 @@ class OrderDetailPage extends ConsumerWidget {
|
||||
);
|
||||
}
|
||||
|
||||
/// Get mock order data for development
|
||||
Map<String, dynamic> _getMockOrder() {
|
||||
return {
|
||||
'orderNumber': 'DH001234',
|
||||
'status': OrderStatus.processing,
|
||||
'statusHistory': [
|
||||
{
|
||||
'title': 'Đơn hàng được tạo',
|
||||
'date': '03/08/2023 - 09:30',
|
||||
'status': 'completed',
|
||||
},
|
||||
{
|
||||
'title': 'Đã xác nhận đơn hàng',
|
||||
'date': '03/08/2023 - 10:15',
|
||||
'status': 'completed',
|
||||
},
|
||||
{
|
||||
'title': 'Đang chuẩn bị hàng',
|
||||
'date': 'Đang thực hiện',
|
||||
'status': 'active',
|
||||
},
|
||||
{
|
||||
'title': 'Vận chuyển',
|
||||
'date': 'Dự kiến: 05/08/2023',
|
||||
'status': 'pending',
|
||||
},
|
||||
{
|
||||
'title': 'Giao hàng thành công',
|
||||
'date': 'Dự kiến: 07/08/2023',
|
||||
'status': 'pending',
|
||||
},
|
||||
/// Build Action Buttons
|
||||
Widget _buildActionButtons(BuildContext context, OrderDetail orderDetail) {
|
||||
final shippingAddress = orderDetail.shippingAddress;
|
||||
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.white,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.1),
|
||||
blurRadius: 15,
|
||||
offset: const Offset(0, -4),
|
||||
),
|
||||
],
|
||||
'deliveryMethod': 'Giao hàng tiêu chuẩn',
|
||||
'warehouseDate': DateTime(2023, 8, 5),
|
||||
'deliveryDate': DateTime(2023, 8, 7),
|
||||
'deliveryAddress':
|
||||
'123 Đường Lê Văn Lương, Phường Tân Hưng,\nQuận 7, TP. Hồ Chí Minh',
|
||||
'receiverName': 'Nguyễn Văn A',
|
||||
'receiverPhone': '0901234567',
|
||||
'customerName': 'Nguyễn Văn A',
|
||||
'customerPhone': '0901234567',
|
||||
'customerEmail': 'nguyenvana@email.com',
|
||||
'customerType': 'Khách VIP',
|
||||
'subtotal': 12900000.0,
|
||||
'shippingFee': 0.0,
|
||||
'discount': 129000.0,
|
||||
'total': 12771000.0,
|
||||
'paymentMethod': 'Chuyển khoản ngân hàng',
|
||||
'notes':
|
||||
'Giao hàng trong giờ hành chính. Vui lòng gọi trước 30 phút khi đến giao hàng.',
|
||||
};
|
||||
),
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
spacing: 12,
|
||||
children: [
|
||||
Expanded(
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: () {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Gọi ${shippingAddress.phone}...'),
|
||||
),
|
||||
);
|
||||
},
|
||||
icon: const FaIcon(FontAwesomeIcons.phone, size: 18),
|
||||
label: const Text('Liên hệ'),
|
||||
style: OutlinedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
side: const BorderSide(
|
||||
color: AppColors.grey100,
|
||||
width: 2,
|
||||
),
|
||||
foregroundColor: AppColors.grey900,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Chức năng đang phát triển...'),
|
||||
),
|
||||
);
|
||||
},
|
||||
icon: const FaIcon(FontAwesomeIcons.penToSquare, size: 18),
|
||||
label: const Text('Cập nhật'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
backgroundColor: AppColors.primaryBlue,
|
||||
foregroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
// 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,15 +179,19 @@ 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,
|
||||
child: statusListAsync.when(
|
||||
data: (statusList) {
|
||||
return ListView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: ListView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
children: [
|
||||
// All filter
|
||||
// All filter (always first)
|
||||
_buildFilterChip(
|
||||
label: 'Tất cả',
|
||||
isSelected: selectedStatus == null,
|
||||
@@ -186,65 +201,52 @@ class _OrdersPageState extends ConsumerState<OrdersPage> {
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
|
||||
// Pending filter
|
||||
_buildFilterChip(
|
||||
label: 'Chờ xác nhận',
|
||||
isSelected: selectedStatus == OrderStatus.pending,
|
||||
// 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(OrderStatus.pending);
|
||||
.selectStatus(status.label);
|
||||
},
|
||||
),
|
||||
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
|
||||
loading: () {
|
||||
// Show minimal loading state or fallback to "All" only
|
||||
return ListView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
scrollDirection: Axis.horizontal,
|
||||
children: [
|
||||
_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);
|
||||
},
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,15 +11,18 @@
|
||||
library;
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:qr_flutter/qr_flutter.dart';
|
||||
|
||||
import 'package:worker/core/constants/ui_constants.dart';
|
||||
import 'package:worker/core/router/app_router.dart';
|
||||
import 'package:worker/core/theme/colors.dart';
|
||||
import 'package:worker/features/orders/presentation/providers/order_repository_provider.dart';
|
||||
|
||||
@@ -47,6 +50,10 @@ class PaymentQrPage extends HookConsumerWidget {
|
||||
final remainingSeconds = useState<int>(900);
|
||||
final timer = useRef<Timer?>(null);
|
||||
|
||||
// Upload state
|
||||
final isUploadingBill = useState<bool>(false);
|
||||
final selectedImagePath = useState<String?>(null);
|
||||
|
||||
// Fetch QR code data
|
||||
useEffect(() {
|
||||
Future<void> fetchQrCode() async {
|
||||
@@ -146,8 +153,27 @@ class PaymentQrPage extends HookConsumerWidget {
|
||||
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
|
||||
// Image Preview Section
|
||||
_buildImagePreviewSection(
|
||||
context,
|
||||
selectedImagePath.value,
|
||||
() async {
|
||||
await _selectImage(context, selectedImagePath);
|
||||
},
|
||||
),
|
||||
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
|
||||
// Action Buttons
|
||||
_buildActionButtons(context),
|
||||
_buildActionButtons(
|
||||
context,
|
||||
ref,
|
||||
isUploadingBill.value,
|
||||
selectedImagePath.value != null,
|
||||
() async {
|
||||
await _uploadBill(context, ref, selectedImagePath, isUploadingBill);
|
||||
},
|
||||
),
|
||||
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
|
||||
@@ -452,44 +478,174 @@ class PaymentQrPage extends HookConsumerWidget {
|
||||
);
|
||||
}
|
||||
|
||||
/// Build image preview section
|
||||
Widget _buildImagePreviewSection(
|
||||
BuildContext context,
|
||||
String? imagePath,
|
||||
VoidCallback onSelectImage,
|
||||
) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
|
||||
padding: const EdgeInsets.all(AppSpacing.md),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(AppRadius.card),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.05),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Ảnh hóa đơn',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF212121),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
|
||||
// Image preview or placeholder
|
||||
InkWell(
|
||||
onTap: onSelectImage,
|
||||
borderRadius: BorderRadius.circular(AppRadius.card),
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
height: 200,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF4F6F8),
|
||||
borderRadius: BorderRadius.circular(AppRadius.card),
|
||||
border: Border.all(
|
||||
color: const Color(0xFFE2E8F0),
|
||||
width: 2,
|
||||
style: BorderStyle.solid,
|
||||
),
|
||||
),
|
||||
child: imagePath != null
|
||||
? ClipRRect(
|
||||
borderRadius: BorderRadius.circular(AppRadius.card - 2),
|
||||
child: Stack(
|
||||
children: [
|
||||
Image.file(
|
||||
File(imagePath),
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
Positioned(
|
||||
top: 8,
|
||||
right: 8,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withValues(alpha: 0.6),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 6,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: const [
|
||||
FaIcon(
|
||||
FontAwesomeIcons.pen,
|
||||
color: Colors.white,
|
||||
size: 12,
|
||||
),
|
||||
SizedBox(width: 6),
|
||||
Text(
|
||||
'Đổi ảnh',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
width: 60,
|
||||
height: 60,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.primaryBlue.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
),
|
||||
child: const Icon(
|
||||
FontAwesomeIcons.image,
|
||||
color: AppColors.primaryBlue,
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const Text(
|
||||
'Chạm để chọn ảnh hóa đơn',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: AppColors.grey500,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
const Text(
|
||||
'Hỗ trợ: JPG, PNG',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppColors.grey500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Build action buttons
|
||||
Widget _buildActionButtons(BuildContext context) {
|
||||
Widget _buildActionButtons(
|
||||
BuildContext context,
|
||||
WidgetRef ref,
|
||||
bool isUploading,
|
||||
bool hasImage,
|
||||
VoidCallback onUpload,
|
||||
) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
|
||||
child: Row(
|
||||
child: Column(
|
||||
children: [
|
||||
// Confirmed Payment Button
|
||||
Expanded(
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: () => _confirmPayment(context),
|
||||
icon: const FaIcon(FontAwesomeIcons.check, size: 18),
|
||||
label: const Text(
|
||||
'Đã thanh toán',
|
||||
style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600),
|
||||
),
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: AppColors.primaryBlue,
|
||||
side: const BorderSide(
|
||||
color: AppColors.primaryBlue,
|
||||
width: 1.5,
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(AppRadius.button),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: AppSpacing.sm),
|
||||
|
||||
// Upload Proof Button
|
||||
Expanded(
|
||||
// Upload Bill Button
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () => _uploadProof(context),
|
||||
icon: const FaIcon(FontAwesomeIcons.camera, size: 18),
|
||||
label: const Text(
|
||||
'Upload bill',
|
||||
style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600),
|
||||
onPressed: (isUploading || !hasImage) ? null : onUpload,
|
||||
icon: isUploading
|
||||
? const SizedBox(
|
||||
width: 18,
|
||||
height: 18,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
|
||||
),
|
||||
)
|
||||
: const FaIcon(FontAwesomeIcons.camera, size: 18),
|
||||
label: Text(
|
||||
isUploading ? 'Đang upload...' : 'Upload bill chuyển khoản',
|
||||
style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w600),
|
||||
),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.primaryBlue,
|
||||
@@ -499,6 +655,33 @@ class PaymentQrPage extends HookConsumerWidget {
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(AppRadius.button),
|
||||
),
|
||||
disabledBackgroundColor: AppColors.grey100,
|
||||
disabledForegroundColor: AppColors.grey500,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.sm),
|
||||
|
||||
// Back to Home Button
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: isUploading ? null : () => context.goNamed(RouteNames.home),
|
||||
icon: const FaIcon(FontAwesomeIcons.house, size: 18),
|
||||
label: const Text(
|
||||
'Quay về trang chủ',
|
||||
style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600),
|
||||
),
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: AppColors.grey900,
|
||||
side: const BorderSide(
|
||||
color: AppColors.grey100,
|
||||
width: 1.5,
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(AppRadius.button),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -598,57 +781,209 @@ class PaymentQrPage extends HookConsumerWidget {
|
||||
);
|
||||
}
|
||||
|
||||
/// Confirm payment
|
||||
void _confirmPayment(BuildContext context) {
|
||||
showDialog<void>(
|
||||
/// Select image for bill
|
||||
Future<void> _selectImage(
|
||||
BuildContext context,
|
||||
ValueNotifier<String?> selectedImagePath,
|
||||
) async {
|
||||
// Show bottom sheet to select camera or gallery
|
||||
final ImageSource? source = await showModalBottomSheet<ImageSource>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Xác nhận thanh toán'),
|
||||
content: const Text(
|
||||
'Bạn đã hoàn tất thanh toán cho đơn hàng này?',
|
||||
style: TextStyle(fontSize: 14),
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(AppRadius.card)),
|
||||
),
|
||||
actions: [
|
||||
builder: (context) => Container(
|
||||
padding: const EdgeInsets.all(AppSpacing.md),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text(
|
||||
'Chọn ảnh hóa đơn',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF212121),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
ListTile(
|
||||
leading: const FaIcon(
|
||||
FontAwesomeIcons.camera,
|
||||
color: AppColors.primaryBlue,
|
||||
),
|
||||
title: const Text('Chụp ảnh'),
|
||||
onTap: () => Navigator.of(context).pop(ImageSource.camera),
|
||||
),
|
||||
ListTile(
|
||||
leading: const FaIcon(
|
||||
FontAwesomeIcons.image,
|
||||
color: AppColors.primaryBlue,
|
||||
),
|
||||
title: const Text('Chọn từ thư viện'),
|
||||
onTap: () => Navigator.of(context).pop(ImageSource.gallery),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.sm),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('Chưa'),
|
||||
child: const Text('Hủy'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
if (source == null || !context.mounted) return;
|
||||
|
||||
try {
|
||||
// Pick image
|
||||
final picker = ImagePicker();
|
||||
final pickedFile = await picker.pickImage(
|
||||
source: source,
|
||||
maxWidth: 1920,
|
||||
maxHeight: 1080,
|
||||
imageQuality: 85,
|
||||
);
|
||||
|
||||
if (pickedFile == null || !context.mounted) return;
|
||||
|
||||
selectedImagePath.value = pickedFile.path;
|
||||
|
||||
// Show success feedback
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Đã xác nhận thanh toán!'),
|
||||
content: Row(
|
||||
children: [
|
||||
FaIcon(FontAwesomeIcons.circleCheck, color: Colors.white, size: 20),
|
||||
SizedBox(width: 12),
|
||||
Text('Đã chọn ảnh. Nhấn "Upload bill chuyển khoản" để gửi.'),
|
||||
],
|
||||
),
|
||||
backgroundColor: AppColors.success,
|
||||
duration: Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
// Navigate back after delay
|
||||
Future.delayed(const Duration(milliseconds: 500), () {
|
||||
if (context.mounted) {
|
||||
context.pop();
|
||||
} catch (e) {
|
||||
if (!context.mounted) return;
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Lỗi khi chọn ảnh: ${e.toString()}'),
|
||||
backgroundColor: AppColors.danger,
|
||||
duration: const Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
}
|
||||
});
|
||||
},
|
||||
}
|
||||
|
||||
/// Upload bill to server
|
||||
Future<void> _uploadBill(
|
||||
BuildContext context,
|
||||
WidgetRef ref,
|
||||
ValueNotifier<String?> selectedImagePath,
|
||||
ValueNotifier<bool> isUploadingBill,
|
||||
) async {
|
||||
if (selectedImagePath.value == null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Vui lòng chọn ảnh hóa đơn trước'),
|
||||
backgroundColor: AppColors.danger,
|
||||
duration: Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Show confirmation dialog
|
||||
final bool? confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Xác nhận upload'),
|
||||
content: const Text(
|
||||
'Bạn có muốn upload hóa đơn này không?',
|
||||
style: TextStyle(fontSize: 14),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(false),
|
||||
child: const Text('Hủy'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () => Navigator.of(context).pop(true),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.primaryBlue,
|
||||
),
|
||||
child: const Text('Đã thanh toán'),
|
||||
child: const Text('Upload'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Upload payment proof
|
||||
void _uploadProof(BuildContext context) {
|
||||
// TODO: Implement image picker and upload
|
||||
if (confirmed != true || !context.mounted) return;
|
||||
|
||||
try {
|
||||
// Start upload
|
||||
isUploadingBill.value = true;
|
||||
|
||||
final repository = await ref.read(orderRepositoryProvider.future);
|
||||
final result = await repository.uploadBill(
|
||||
filePath: selectedImagePath.value!,
|
||||
orderId: orderId,
|
||||
);
|
||||
|
||||
if (!context.mounted) return;
|
||||
|
||||
isUploadingBill.value = false;
|
||||
|
||||
// Show success message and navigate
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Tính năng upload bill đang được phát triển'),
|
||||
content: Row(
|
||||
children: [
|
||||
FaIcon(FontAwesomeIcons.circleCheck, color: Colors.white, size: 20),
|
||||
SizedBox(width: 12),
|
||||
Text('Upload hóa đơn thành công!'),
|
||||
],
|
||||
),
|
||||
backgroundColor: AppColors.success,
|
||||
duration: Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
|
||||
// Navigate to order success page after successful upload
|
||||
Future.delayed(const Duration(milliseconds: 500), () {
|
||||
if (context.mounted) {
|
||||
context.pushReplacementNamed(
|
||||
RouteNames.orderSuccess,
|
||||
queryParameters: {
|
||||
'orderNumber': orderId,
|
||||
'total': amount.toString(),
|
||||
'isNegotiation': 'false',
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
if (!context.mounted) return;
|
||||
|
||||
isUploadingBill.value = false;
|
||||
|
||||
// Show error message
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Row(
|
||||
children: [
|
||||
const FaIcon(FontAwesomeIcons.circleXmark, color: Colors.white, size: 20),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text('Lỗi upload: ${e.toString()}'),
|
||||
),
|
||||
],
|
||||
),
|
||||
backgroundColor: AppColors.danger,
|
||||
duration: const Duration(seconds: 3),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Format currency
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -97,4 +97,4 @@ final class OrderRepositoryProvider
|
||||
}
|
||||
}
|
||||
|
||||
String _$orderRepositoryHash() => r'985408a6667ab31427524f9b1981287c28f4f221';
|
||||
String _$orderRepositoryHash() => r'd1b811cb1849e44c48ce02d7bb620de1b0ccdfb8';
|
||||
|
||||
@@ -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,42 @@
|
||||
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_detail.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 +51,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 +91,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 +111,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 +140,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 +170,22 @@ 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();
|
||||
}
|
||||
|
||||
/// Order Detail Provider
|
||||
///
|
||||
/// Provides detailed order information by order ID.
|
||||
@riverpod
|
||||
Future<OrderDetail> orderDetail(Ref ref, String orderId) async {
|
||||
final repository = await ref.watch(orderRepositoryProvider.future);
|
||||
return await repository.getOrderDetail(orderId);
|
||||
}
|
||||
|
||||
@@ -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,152 @@ 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';
|
||||
|
||||
/// Order Detail Provider
|
||||
///
|
||||
/// Provides detailed order information by order ID.
|
||||
|
||||
@ProviderFor(orderDetail)
|
||||
const orderDetailProvider = OrderDetailFamily._();
|
||||
|
||||
/// Order Detail Provider
|
||||
///
|
||||
/// Provides detailed order information by order ID.
|
||||
|
||||
final class OrderDetailProvider
|
||||
extends
|
||||
$FunctionalProvider<
|
||||
AsyncValue<OrderDetail>,
|
||||
OrderDetail,
|
||||
FutureOr<OrderDetail>
|
||||
>
|
||||
with $FutureModifier<OrderDetail>, $FutureProvider<OrderDetail> {
|
||||
/// Order Detail Provider
|
||||
///
|
||||
/// Provides detailed order information by order ID.
|
||||
const OrderDetailProvider._({
|
||||
required OrderDetailFamily super.from,
|
||||
required String super.argument,
|
||||
}) : super(
|
||||
retry: null,
|
||||
name: r'orderDetailProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$orderDetailHash();
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return r'orderDetailProvider'
|
||||
''
|
||||
'($argument)';
|
||||
}
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$FutureProviderElement<OrderDetail> $createElement(
|
||||
$ProviderPointer pointer,
|
||||
) => $FutureProviderElement(pointer);
|
||||
|
||||
@override
|
||||
FutureOr<OrderDetail> create(Ref ref) {
|
||||
final argument = this.argument as String;
|
||||
return orderDetail(ref, argument);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is OrderDetailProvider && other.argument == argument;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return argument.hashCode;
|
||||
}
|
||||
}
|
||||
|
||||
String _$orderDetailHash() => r'628b9102b54579b8bba5f9135d875730cf2066c0';
|
||||
|
||||
/// Order Detail Provider
|
||||
///
|
||||
/// Provides detailed order information by order ID.
|
||||
|
||||
final class OrderDetailFamily extends $Family
|
||||
with $FunctionalFamilyOverride<FutureOr<OrderDetail>, String> {
|
||||
const OrderDetailFamily._()
|
||||
: super(
|
||||
retry: null,
|
||||
name: r'orderDetailProvider',
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
isAutoDispose: true,
|
||||
);
|
||||
|
||||
/// Order Detail Provider
|
||||
///
|
||||
/// Provides detailed order information by order ID.
|
||||
|
||||
OrderDetailProvider call(String orderId) =>
|
||||
OrderDetailProvider._(argument: orderId, from: this);
|
||||
|
||||
@override
|
||||
String toString() => r'orderDetailProvider';
|
||||
}
|
||||
|
||||
@@ -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