create order

This commit is contained in:
Phuoc Nguyen
2025-11-21 16:50:43 +07:00
parent f2f95849d4
commit 4913a4e04b
31 changed files with 1696 additions and 187 deletions

View File

@@ -23,7 +23,7 @@ class InvoicesLocalDataSource {
throw Exception('Invalid JSON format: expected List');
}
final invoices = (decoded as List<dynamic>)
final invoices = decoded
.map((json) => InvoiceModel.fromJson(json as Map<String, dynamic>))
.toList();

View File

@@ -0,0 +1,157 @@
/// Order Remote Data Source
///
/// Handles API calls for order-related data.
library;
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';
import 'package:worker/features/orders/data/models/payment_term_model.dart';
/// Order Remote Data Source
class OrderRemoteDataSource {
const OrderRemoteDataSource(this._dioClient);
final DioClient _dioClient;
/// Get order status list
///
/// Calls: POST /api/method/building_material.building_material.api.sales_order.get_order_status_list
/// Returns: List of order statuses with labels and colors
Future<List<OrderStatusModel>> getOrderStatusList() async {
try {
final response = await _dioClient.post<Map<String, dynamic>>(
'${ApiConstants.frappeApiMethod}${ApiConstants.getOrderStatusList}',
data: <String, dynamic>{},
);
final data = response.data;
if (data == null) {
throw Exception('No data received from getOrderStatusList API');
}
// API returns: { "message": [...] }
final message = data['message'];
if (message == null) {
throw Exception('No message field in getOrderStatusList response');
}
final List<dynamic> statusList = message as List<dynamic>;
return statusList
.map((json) => OrderStatusModel.fromJson(json as Map<String, dynamic>))
.toList();
} catch (e) {
throw Exception('Failed to get order status list: $e');
}
}
/// Get payment terms list
///
/// Calls: POST /api/method/frappe.client.get_list
/// Body: { "doctype": "Payment Terms Template", "fields": ["name","custom_description"], "limit_page_length": 0 }
/// Returns: List of payment terms with names and descriptions
Future<List<PaymentTermModel>> getPaymentTermsList() async {
try {
final response = await _dioClient.post<Map<String, dynamic>>(
'${ApiConstants.frappeApiMethod}${ApiConstants.frappeGetList}',
data: {
'doctype': 'Payment Terms Template',
'fields': ['name', 'custom_description'],
'limit_page_length': 0,
},
);
final data = response.data;
if (data == null) {
throw Exception('No data received from getPaymentTermsList API');
}
// API returns: { "message": [...] }
final message = data['message'];
if (message == null) {
throw Exception('No message field in getPaymentTermsList response');
}
final List<dynamic> termsList = message as List<dynamic>;
return termsList
.map((json) => PaymentTermModel.fromJson(json as Map<String, dynamic>))
.toList();
} catch (e) {
throw Exception('Failed to get payment terms list: $e');
}
}
/// Create new order
///
/// Calls: POST /api/method/building_material.building_material.api.sales_order.save
/// Body: {
/// "transaction_date": "2025-11-20",
/// "delivery_date": "2025-11-20",
/// "shipping_address_name": "...",
/// "customer_address": "...",
/// "description": "...",
/// "payment_terms": "...",
/// "items": [{"item_id": "...", "qty_entered": 0, "primary_qty": 0, "price_entered": 0}]
/// }
/// Returns: { "message": { "name": "SAL-ORD-2025-00001", ... } }
Future<Map<String, dynamic>> createOrder({
required List<Map<String, dynamic>> items,
required Map<String, dynamic> deliveryAddress,
required String paymentMethod,
bool needsInvoice = false,
bool needsNegotiation = false,
String? notes,
}) async {
try {
// Get current date for transaction and delivery
final now = DateTime.now();
final dateStr = '${now.year}-${now.month.toString().padLeft(2, '0')}-${now.day.toString().padLeft(2, '0')}';
// Format items for Frappe API
final formattedItems = items.map((item) {
return {
'item_id': item['item_id'],
'qty_entered': item['quantity'], // boxes/pieces (viên)
'primary_qty': item['quantityConverted'], // m²
'price_entered': item['price'],
};
}).toList();
// Prepare request body in Frappe format
final requestBody = {
'transaction_date': dateStr,
'delivery_date': dateStr,
'shipping_address_name': deliveryAddress['name'] ?? '',
'customer_address': deliveryAddress['name'] ?? '',
'description': notes ?? 'Order from mobile app',
'payment_terms': paymentMethod,
'items': formattedItems,
};
final response = await _dioClient.post<Map<String, dynamic>>(
'${ApiConstants.frappeApiMethod}${ApiConstants.createOrder}',
data: requestBody,
);
final data = response.data;
if (data == null) {
throw Exception('No data received from createOrder API');
}
// Extract order info from Frappe response
final message = data['message'] as Map<String, dynamic>?;
if (message == null) {
throw Exception('No message field in createOrder response');
}
// Return standardized response
return {
'orderId': message['name'] ?? '',
'orderNumber': message['name'] ?? '',
'fullResponse': message,
};
} catch (e) {
throw Exception('Failed to create order: $e');
}
}
}

View File

@@ -24,7 +24,7 @@ class OrdersLocalDataSource {
throw Exception('Invalid JSON format: expected List');
}
final orders = (decoded as List<dynamic>)
final orders = decoded
.map((json) => OrderModel.fromJson(json as Map<String, dynamic>))
.toList();

View File

@@ -0,0 +1,65 @@
/// Order Status Model
///
/// Data model for order status from API responses.
library;
import 'package:equatable/equatable.dart';
import 'package:worker/features/orders/domain/entities/order_status.dart';
/// Order Status Model
class OrderStatusModel extends Equatable {
final String status;
final String label;
final String color;
final int index;
const OrderStatusModel({
required this.status,
required this.label,
required this.color,
required this.index,
});
/// Create from JSON
factory OrderStatusModel.fromJson(Map<String, dynamic> json) {
return OrderStatusModel(
status: json['status'] as String,
label: json['label'] as String,
color: json['color'] as String,
index: json['index'] as int,
);
}
/// Convert to JSON
Map<String, dynamic> toJson() {
return {
'status': status,
'label': label,
'color': color,
'index': index,
};
}
/// Convert to entity
OrderStatus toEntity() {
return OrderStatus(
status: status,
label: label,
color: color,
index: index,
);
}
/// Create from entity
factory OrderStatusModel.fromEntity(OrderStatus entity) {
return OrderStatusModel(
status: entity.status,
label: entity.label,
color: entity.color,
index: entity.index,
);
}
@override
List<Object?> get props => [status, label, color, index];
}

View File

@@ -0,0 +1,53 @@
/// Payment Term Model
///
/// Data model for payment term from API responses.
library;
import 'package:equatable/equatable.dart';
import 'package:worker/features/orders/domain/entities/payment_term.dart';
/// Payment Term Model
class PaymentTermModel extends Equatable {
final String name;
final String? customDescription;
const PaymentTermModel({
required this.name,
this.customDescription,
});
/// Create from JSON
factory PaymentTermModel.fromJson(Map<String, dynamic> json) {
return PaymentTermModel(
name: json['name'] as String,
customDescription: json['custom_description'] as String?,
);
}
/// Convert to JSON
Map<String, dynamic> toJson() {
return {
'name': name,
'custom_description': customDescription,
};
}
/// Convert to entity
PaymentTerm toEntity() {
return PaymentTerm(
name: name,
customDescription: customDescription ?? '',
);
}
/// Create from entity
factory PaymentTermModel.fromEntity(PaymentTerm entity) {
return PaymentTermModel(
name: entity.name,
customDescription: entity.customDescription,
);
}
@override
List<Object?> get props => [name, customDescription];
}

View File

@@ -0,0 +1,59 @@
/// Order Repository Implementation
///
/// Implements the order repository interface.
library;
import 'package:worker/features/orders/data/datasources/order_remote_datasource.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);
final OrderRemoteDataSource _remoteDataSource;
@override
Future<List<OrderStatus>> getOrderStatusList() async {
try {
final models = await _remoteDataSource.getOrderStatusList();
return models.map((model) => model.toEntity()).toList();
} catch (e) {
throw Exception('Failed to get order status list: $e');
}
}
@override
Future<List<PaymentTerm>> getPaymentTermsList() async {
try {
final models = await _remoteDataSource.getPaymentTermsList();
return models.map((model) => model.toEntity()).toList();
} catch (e) {
throw Exception('Failed to get payment terms list: $e');
}
}
@override
Future<Map<String, dynamic>> createOrder({
required List<Map<String, dynamic>> items,
required Map<String, dynamic> deliveryAddress,
required String paymentMethod,
bool needsInvoice = false,
bool needsNegotiation = false,
String? notes,
}) async {
try {
return await _remoteDataSource.createOrder(
items: items,
deliveryAddress: deliveryAddress,
paymentMethod: paymentMethod,
needsInvoice: needsInvoice,
needsNegotiation: needsNegotiation,
notes: notes,
);
} catch (e) {
throw Exception('Failed to create order: $e');
}
}
}