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

90
docs/order.sh Normal file
View File

@@ -0,0 +1,90 @@
#Get list of order status
curl --location --request POST 'https://land.dbiz.com//api/method/building_material.building_material.api.sales_order.get_order_status_list' \
--header 'Cookie: sid=a98c0b426abd8af3b0fd92407ef96937acda888a9a63bf3c580447d4; full_name=Hsadqdqwed; sid=42d89a7465571e04e0ee47a5bb1dd73563ff4f30ef9f7370ed490275; system_user=no; user_id=123%40gmail.com; user_image=/files/avatar_0987654321_1763631288.jpg' \
--header 'X-Frappe-Csrf-Token: a2bc5e9342441ff895ad2781e99a4c3fae4cad1250ae40c51f90067a' \
--header 'Content-Type: application/json' \
--data ''
#Response list of order status
{
"message": [
{
"status": "Pending approval",
"label": "Chờ phê duyệt",
"color": "Warning",
"index": 1
},
{
"status": "Processing",
"label": "Đang xử lý",
"color": "Warning",
"index": 2
},
{
"status": "Completed",
"label": "Hoàn thành",
"color": "Success",
"index": 3
},
{
"status": "Rejected",
"label": "Từ chối",
"color": "Danger",
"index": 4
},
{
"status": "Cancelled",
"label": "HỦY BỎ",
"color": "Danger",
"index": 5
}
]
}
#get payment list
curl --location 'https://land.dbiz.com//api/method/frappe.client.get_list' \
--header 'X-Frappe-Csrf-Token: a2bc5e9342441ff895ad2781e99a4c3fae4cad1250ae40c51f90067a' \
--header 'Cookie: sid=a98c0b426abd8af3b0fd92407ef96937acda888a9a63bf3c580447d4; full_name=phuoc; sid=a98c0b426abd8af3b0fd92407ef96937acda888a9a63bf3c580447d4; system_user=no; user_id=vodanh.2901%40gmail.com; user_image=https%3A//secure.gravatar.com/avatar/753a0e2601b9bd87aed417e2ad123bf8%3Fd%3D404%26s%3D200' \
--header 'Content-Type: application/json' \
--data '{
"doctype": "Payment Terms Template",
"fields": ["name","custom_description"],
"limit_page_length": 0
}'
#response payment list
{
"message": [
{
"name": "Thanh toán hoàn toàn",
"custom_description": "Thanh toán ngay được chiết khấu 2%"
},
{
"name": "Thanh toán trả trước",
"custom_description": "Trả trước (≥20%), còn lại thanh toán trong vòng 30 ngày"
}
]
}
#create order
curl --location 'https://land.dbiz.com//api/method/building_material.building_material.api.sales_order.save' \
--header 'Cookie: sid=a98c0b426abd8af3b0fd92407ef96937acda888a9a63bf3c580447d4; full_name=phuoc; sid=c0f46dc2ed23d58c013daa7d1813b36caf04555472b792cdb74e0d61; system_user=no; user_id=vodanh.2901%40gmail.com; user_image=https%3A//secure.gravatar.com/avatar/753a0e2601b9bd87aed417e2ad123bf8%3Fd%3D404%26s%3D200' \
--header 'X-Frappe-Csrf-Token: a2bc5e9342441ff895ad2781e99a4c3fae4cad1250ae40c51f90067a' \
--header 'Content-Type: application/json' \
--data '{
"transaction_date": "2025-11-20", // Ngày tạo
"delivery_date": "2025-11-20", // Ngày dự kiến giao
"shipping_address_name": "Lam Address-Billing",
"customer_address": "Lam Address-Billing",
"description": "Order description", // Ghi chú
"payment_terms" : "Thanh toán hoàn toàn", // Lấy name từ GET PAYMENT TERM
"items": [
{
"item_id": "HOA E02",
"qty_entered": 2, // SỐ lượng User tự nhập
"primary_qty" : 2.56, // SỐ lượng sau khi quy đổi
"price_entered": 10000 // Đơn giá
}
]
}'

View File

@@ -16,7 +16,7 @@
<i class="fas fa-check"></i>
</div>
<h1 class="success-title">Đặt hàng thành công!</h1>
<h1 class="success-title">Tạo đơn hàng thành công!</h1>
<p class="success-message">
Cảm ơn bạn đã đặt hàng. Chúng tôi sẽ liên hệ xác nhận trong vòng 24 giờ.
</p>
@@ -46,7 +46,7 @@
</div>
<!-- Next Steps -->
<div class="card">
<!-- <div class="card">
<h3 class="card-title">Các bước tiếp theo</h3>
<div style="display: flex; align-items: flex-start; margin-bottom: 12px;">
<div style="width: 24px; height: 24px; background: var(--primary-blue); color: white; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 12px; font-weight: 700; margin-right: 12px; flex-shrink: 0;">1</div>
@@ -69,7 +69,7 @@
<p class="text-small text-muted">Vận chuyển đến địa chỉ của bạn</p>
</div>
</div>
</div>
</div>-->
<!-- Action Buttons -->
<a href="#" class="btn btn-primary btn-block mb-2">

View File

@@ -122,7 +122,7 @@
<div class="info-row">
<span class="info-label">Nội dung:</span>
<span class="info-value">DH001234 La Nguyen Quynh</span>
<span class="info-value">DH001234</span>
<button class="copy-btn" onclick="copyText('DH001234 La Nguyen Quynh')">
<i class="fas fa-copy"></i>
</button>
@@ -139,12 +139,15 @@
<!-- Action Buttons -->
<div class="action-buttons">
<button class="btn btn-secondary" onclick="confirmPayment()">
<!--<button class="btn btn-secondary" onclick="confirmPayment()">
<i class="fas fa-check"></i> Đã thanh toán
</button>
</button>-->
<button class="btn btn-primary" onclick="uploadProof()">
<i class="fas fa-camera"></i> Upload bill chuyển khoản
</button>
<a href="index.html" class="btn btn-secondary btn-block">
<i class="fas fa-home"></i> Quay về trang chủ
</a>
</div>
<!-- Timer -->

View File

@@ -211,10 +211,15 @@ class ApiConstants {
// Order Endpoints
// ============================================================================
/// Create new order
/// POST /orders
/// Body: { "items": [...], "deliveryAddress": {...}, "paymentMethod": "..." }
static const String createOrder = '/orders';
/// Get order status list (requires sid and csrf_token)
/// POST /api/method/building_material.building_material.api.sales_order.get_order_status_list
/// Returns: { "message": [{ "status": "...", "label": "...", "color": "...", "index": 0 }] }
static const String getOrderStatusList = '/building_material.building_material.api.sales_order.get_order_status_list';
/// Create new order (requires sid and csrf_token)
/// POST /api/method/building_material.building_material.api.sales_order.save
/// Body: { "transaction_date": "...", "delivery_date": "...", "items": [...], ... }
static const String createOrder = '/building_material.building_material.api.sales_order.save';
/// Get user's orders
/// GET /orders?status={status}&page={page}&limit={limit}

View File

@@ -32,6 +32,7 @@ import 'package:worker/features/main/presentation/pages/main_scaffold.dart';
import 'package:worker/features/news/presentation/pages/news_detail_page.dart';
import 'package:worker/features/news/presentation/pages/news_list_page.dart';
import 'package:worker/features/orders/presentation/pages/order_detail_page.dart';
import 'package:worker/features/orders/presentation/pages/order_success_page.dart';
import 'package:worker/features/orders/presentation/pages/orders_page.dart';
import 'package:worker/features/orders/presentation/pages/payment_detail_page.dart';
import 'package:worker/features/orders/presentation/pages/payment_qr_page.dart';
@@ -327,6 +328,29 @@ final routerProvider = Provider<GoRouter>((ref) {
},
),
// Order Success Route
GoRoute(
path: RouteNames.orderSuccess,
name: RouteNames.orderSuccess,
pageBuilder: (context, state) {
final orderNumber = state.uri.queryParameters['orderNumber'] ?? '';
final totalStr = state.uri.queryParameters['total'];
final total = totalStr != null ? double.tryParse(totalStr) : null;
final paymentMethod = state.uri.queryParameters['paymentMethod'];
final isNegotiationStr = state.uri.queryParameters['isNegotiation'];
final isNegotiation = isNegotiationStr == 'true';
return MaterialPage(
key: state.pageKey,
child: OrderSuccessPage(
orderNumber: orderNumber,
total: total,
paymentMethod: paymentMethod,
isNegotiation: isNegotiation,
),
);
},
),
// Quotes Route
GoRoute(
path: RouteNames.quotes,

View File

@@ -42,7 +42,7 @@ class _LoginPageState extends ConsumerState<LoginPage> {
final _formKey = GlobalKey<FormState>();
// Controllers
final _phoneController = TextEditingController(text: "0978113710");
final _phoneController = TextEditingController(text: "0986788766");
final _passwordController = TextEditingController(text: "123456");
// Focus nodes

View File

@@ -26,6 +26,8 @@ import 'package:worker/features/cart/presentation/widgets/invoice_section.dart';
import 'package:worker/features/cart/presentation/widgets/order_summary_section.dart';
import 'package:worker/features/cart/presentation/widgets/payment_method_section.dart';
import 'package:worker/features/cart/presentation/widgets/price_negotiation_section.dart';
import 'package:worker/features/orders/presentation/providers/order_status_provider.dart';
import 'package:worker/features/orders/presentation/providers/payment_terms_provider.dart';
/// Checkout Page
///
@@ -51,12 +53,15 @@ class CheckoutPage extends HookConsumerWidget {
// Invoice section
final needsInvoice = useState<bool>(false);
// Payment method
final paymentMethod = useState<String>('full_payment');
// Payment method (will be set to first payment term name from API)
final paymentMethod = useState<String>('');
// Price negotiation
final needsNegotiation = useState<bool>(false);
// Watch API provider for payment terms
final paymentTermsListAsync = ref.watch(paymentTermsListProvider);
// Get CartItemData from navigation
final cartItemsData = checkoutData?['cartItems'] as List<dynamic>? ?? [];
@@ -85,7 +90,7 @@ class CheckoutPage extends HookConsumerWidget {
);
// TODO: Fetch member discount from user profile API
const memberDiscountPercent = 15.0; // Diamond tier (temporary)
const memberDiscountPercent = 0.0; // Temporarily disabled (was 15.0 for Diamond tier)
final memberDiscount = subtotal * (memberDiscountPercent / 100);
// TODO: Fetch shipping fee from API based on address
@@ -140,7 +145,78 @@ class CheckoutPage extends HookConsumerWidget {
// Payment Method Section (hidden if negotiation is checked)
if (!needsNegotiation.value)
PaymentMethodSection(paymentMethod: paymentMethod),
paymentTermsListAsync.when(
data: (paymentTerms) {
// Set default payment method to first term if not set
if (paymentMethod.value.isEmpty && paymentTerms.isNotEmpty) {
WidgetsBinding.instance.addPostFrameCallback((_) {
paymentMethod.value = paymentTerms.first.name;
});
}
return PaymentMethodSection(
paymentMethod: paymentMethod,
paymentTerms: paymentTerms,
);
},
loading: () => 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: const Center(
child: CircularProgressIndicator(),
),
),
error: (error, stack) => 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(
children: [
const Icon(
FontAwesomeIcons.triangleExclamation,
color: AppColors.danger,
size: 32,
),
const SizedBox(height: 12),
Text(
'Không thể tải phương thức thanh toán',
style: const TextStyle(
fontSize: 14,
color: AppColors.grey500,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
TextButton(
onPressed: () {
ref.invalidate(paymentTermsListProvider);
},
child: const Text('Thử lại'),
),
],
),
),
),
if (!needsNegotiation.value)
const SizedBox(height: AppSpacing.md),
@@ -164,6 +240,7 @@ class CheckoutPage extends HookConsumerWidget {
// Price Negotiation Section
PriceNegotiationSection(needsNegotiation: needsNegotiation),
const SizedBox(height: AppSpacing.md),
// Terms and Conditions
@@ -200,6 +277,8 @@ class CheckoutPage extends HookConsumerWidget {
selectedAddress: selectedAddress.value,
paymentMethod: paymentMethod.value,
total: total,
cartItems: checkoutItems,
notes: notesController.text.trim().isEmpty ? null : notesController.text.trim(),
),
const SizedBox(height: AppSpacing.lg),

View File

@@ -8,11 +8,7 @@ import 'package:worker/features/products/domain/entities/product.dart';
/// Cart Item Data
///
/// Represents a product in the cart with quantity.
class CartItemData {
final Product product;
final double quantity;
final double quantityConverted; // Rounded-up quantity for actual billing
final int boxes; // Number of tiles/boxes needed
class CartItemData { // Number of tiles/boxes needed
const CartItemData({
required this.product,
@@ -20,6 +16,10 @@ class CartItemData {
required this.quantityConverted,
required this.boxes,
});
final Product product;
final double quantity;
final double quantityConverted; // Rounded-up quantity for actual billing
final int boxes;
/// Calculate line total using CONVERTED quantity (important for accurate billing)
double get lineTotal => product.basePrice * quantityConverted;
@@ -43,19 +43,6 @@ class CartItemData {
///
/// Represents the complete state of the shopping cart.
class CartState {
final List<CartItemData> items;
final Map<String, bool> selectedItems; // productId -> isSelected
final String selectedWarehouse;
final String? discountCode;
final bool discountCodeApplied;
final String memberTier;
final double memberDiscountPercent;
final double subtotal;
final double memberDiscount;
final double shippingFee;
final double total;
final bool isLoading;
final String? errorMessage;
const CartState({
required this.items,
@@ -88,6 +75,19 @@ class CartState {
total: 0.0,
);
}
final List<CartItemData> items;
final Map<String, bool> selectedItems; // productId -> isSelected
final String selectedWarehouse;
final String? discountCode;
final bool discountCodeApplied;
final String memberTier;
final double memberDiscountPercent;
final double subtotal;
final double memberDiscount;
final double shippingFee;
final double total;
final bool isLoading;
final String? errorMessage;
bool get isEmpty => items.isEmpty;
bool get isNotEmpty => items.isNotEmpty;

View File

@@ -5,16 +5,18 @@ library;
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.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/account/domain/entities/address.dart';
import 'package:worker/features/orders/presentation/providers/order_repository_provider.dart';
/// Checkout Submit Button
///
/// Button that changes based on negotiation checkbox state.
class CheckoutSubmitButton extends StatelessWidget {
class CheckoutSubmitButton extends HookConsumerWidget {
const CheckoutSubmitButton({
super.key,
required this.formKey,
@@ -23,6 +25,8 @@ class CheckoutSubmitButton extends StatelessWidget {
required this.selectedAddress,
required this.paymentMethod,
required this.total,
required this.cartItems,
this.notes,
});
final GlobalKey<FormState> formKey;
@@ -31,9 +35,11 @@ class CheckoutSubmitButton extends StatelessWidget {
final Address? selectedAddress;
final String paymentMethod;
final double total;
final List<Map<String, dynamic>> cartItems;
final String? notes;
@override
Widget build(BuildContext context) {
Widget build(BuildContext context, WidgetRef ref) {
return Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
@@ -52,7 +58,7 @@ class CheckoutSubmitButton extends StatelessWidget {
}
if (formKey.currentState?.validate() ?? false) {
_handlePlaceOrder(context);
_handlePlaceOrder(context, ref);
}
},
style: ElevatedButton.styleFrom(
@@ -78,48 +84,98 @@ class CheckoutSubmitButton extends StatelessWidget {
}
/// Handle place order
void _handlePlaceOrder(BuildContext context) {
// TODO: Implement actual order placement with backend
if (needsNegotiation) {
// Show negotiation request sent message
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Yêu cầu đàm phán giá đã được gửi!'),
backgroundColor: AppColors.success,
duration: Duration(seconds: 2),
Future<void> _handlePlaceOrder(BuildContext context, WidgetRef ref) async {
// Show loading indicator
showDialog<void>(
context: context,
barrierDismissible: false,
builder: (context) => const Center(
child: CircularProgressIndicator(),
),
);
// Navigate back after a short delay
Future.delayed(const Duration(milliseconds: 500), () {
try {
// Prepare delivery address data
final deliveryAddressData = {
'name': selectedAddress!.name,
'phone': selectedAddress!.phone,
'street': selectedAddress!.addressLine1,
'ward': selectedAddress!.wardName ?? selectedAddress!.wardCode,
'city': selectedAddress!.cityName ?? selectedAddress!.cityCode,
};
// Prepare items data for API
// quantity = boxes/pieces (viên), quantityConverted = m²
// price = quantityConverted * basePrice
final itemsData = cartItems.map((item) {
return {
'item_id': item['sku'] as String,
'quantity': item['quantity'] as double,
'quantityConverted': item['quantityConverted'] as double,
'price': (item['quantityConverted'] as double) * (item['price'] as double),
};
}).toList();
// Call create order API
final result = await ref.read(createOrderProvider(
items: itemsData,
deliveryAddress: deliveryAddressData,
paymentMethod: paymentMethod,
needsInvoice: needsInvoice,
needsNegotiation: needsNegotiation,
notes: notes,
).future);
// Close loading dialog
if (context.mounted) {
context.pop();
Navigator.of(context).pop();
}
});
} else {
// Generate order ID (mock - replace with actual from backend)
final orderId =
// Extract order number from response
final orderNumber = result['orderNumber'] as String? ??
result['orderId'] as String? ??
'DH${DateTime.now().millisecondsSinceEpoch.toString().substring(7)}';
// Show order success message
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Đặt hàng thành công! Chuyển đến thanh toán...'),
backgroundColor: AppColors.success,
duration: Duration(seconds: 1),
),
);
// Navigate to payment QR page after a short delay
Future.delayed(const Duration(milliseconds: 500), () {
if (needsNegotiation) {
// Navigate to order success page with negotiation flag
if (context.mounted) {
context.pushNamed(
RouteNames.paymentQr,
queryParameters: {'orderId': orderId, 'amount': total.toString()},
context.pushReplacementNamed(
RouteNames.orderSuccess,
queryParameters: {
'orderNumber': orderNumber,
'total': total.toString(),
'isNegotiation': 'true',
},
);
}
} else {
// Navigate to payment QR page
if (context.mounted) {
context.pushReplacementNamed(
RouteNames.paymentQr,
queryParameters: {
'orderId': orderNumber,
'amount': total.toString(),
},
);
}
}
} catch (e) {
// Close loading dialog
if (context.mounted) {
Navigator.of(context).pop();
}
// Show error message
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Lỗi khi tạo đơn hàng: ${e.toString()}'),
backgroundColor: AppColors.danger,
duration: const Duration(seconds: 3),
),
);
}
});
}
}
}

View File

@@ -1,8 +1,6 @@
/// Payment Method Section Widget
///
/// Payment method selection with two options:
/// 1. Full payment via bank transfer
/// 2. Partial payment (>=20%, 30 day terms)
/// Payment method selection with dynamic options from API.
library;
import 'package:flutter/material.dart';
@@ -10,17 +8,51 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:worker/core/constants/ui_constants.dart';
import 'package:worker/core/theme/colors.dart';
import 'package:worker/features/orders/domain/entities/payment_term.dart';
/// Payment Method Section
///
/// Two payment options matching checkout.html design.
/// Displays payment options from API matching checkout.html design.
class PaymentMethodSection extends HookWidget {
final ValueNotifier<String> paymentMethod;
final List<PaymentTerm> paymentTerms;
const PaymentMethodSection({super.key, required this.paymentMethod});
const PaymentMethodSection({
super.key,
required this.paymentMethod,
required this.paymentTerms,
});
@override
Widget build(BuildContext context) {
// Show empty state if no payment terms available
if (paymentTerms.isEmpty) {
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: const Center(
child: Text(
'Không có phương thức thanh toán khả dụng',
style: TextStyle(
fontSize: 14,
color: AppColors.grey500,
),
),
),
);
}
return Container(
margin: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
padding: const EdgeInsets.all(AppSpacing.md),
@@ -50,15 +82,29 @@ class PaymentMethodSection extends HookWidget {
const SizedBox(height: AppSpacing.md),
// Full Payment Option
// Dynamic Payment Options from API
...paymentTerms.asMap().entries.map((entry) {
final index = entry.key;
final term = entry.value;
// Choose icon based on payment term name
IconData icon = FontAwesomeIcons.buildingColumns;
if (term.name.toLowerCase().contains('trả trước') ||
term.name.toLowerCase().contains('một phần')) {
icon = FontAwesomeIcons.creditCard;
}
return Column(
children: [
if (index > 0) const Divider(height: 1),
InkWell(
onTap: () => paymentMethod.value = 'full_payment',
onTap: () => paymentMethod.value = term.name,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Row(
children: [
Radio<String>(
value: 'full_payment',
value: term.name,
groupValue: paymentMethod.value,
onChanged: (value) {
paymentMethod.value = value!;
@@ -66,27 +112,27 @@ class PaymentMethodSection extends HookWidget {
activeColor: AppColors.primaryBlue,
),
const SizedBox(width: 12),
const Icon(
FontAwesomeIcons.buildingColumns,
Icon(
icon,
color: AppColors.grey500,
size: 24,
),
const SizedBox(width: 12),
const Expanded(
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Thanh toán hoàn toàn',
style: TextStyle(
term.name,
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.w500,
),
),
SizedBox(height: 4),
const SizedBox(height: 4),
Text(
'Thanh toán qua tài khoản ngân hàng',
style: TextStyle(
term.customDescription,
style: const TextStyle(
fontSize: 13,
color: AppColors.grey500,
),
@@ -98,57 +144,9 @@ class PaymentMethodSection extends HookWidget {
),
),
),
const Divider(height: 1),
// Partial Payment Option
InkWell(
onTap: () => paymentMethod.value = 'partial_payment',
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Row(
children: [
Radio<String>(
value: 'partial_payment',
groupValue: paymentMethod.value,
onChanged: (value) {
paymentMethod.value = value!;
},
activeColor: AppColors.primaryBlue,
),
const SizedBox(width: 12),
const Icon(
FontAwesomeIcons.creditCard,
color: AppColors.grey500,
size: 24,
),
const SizedBox(width: 12),
const Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Thanh toán một phần',
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w500,
),
),
SizedBox(height: 4),
Text(
'Trả trước(≥20%), còn lại thanh toán trong vòng 30 ngày',
style: TextStyle(
fontSize: 13,
color: AppColors.grey500,
),
),
],
),
),
],
),
),
),
);
}),
],
),
);

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

View File

@@ -0,0 +1,31 @@
/// Order Status Entity
///
/// Represents an order status option from the API.
library;
import 'package:equatable/equatable.dart';
/// Order Status Entity
class OrderStatus extends Equatable {
/// Status value (e.g., "Pending approval", "Processing", "Completed")
final String status;
/// Vietnamese label (e.g., "Chờ phê duyệt", "Đang xử lý", "Hoàn thành")
final String label;
/// Color indicator (e.g., "Warning", "Success", "Danger")
final String color;
/// Display order index
final int index;
const OrderStatus({
required this.status,
required this.label,
required this.color,
required this.index,
});
@override
List<Object?> get props => [status, label, color, index];
}

View File

@@ -0,0 +1,23 @@
/// Payment Term Entity
///
/// Represents a payment term template option from the API.
library;
import 'package:equatable/equatable.dart';
/// Payment Term Entity
class PaymentTerm extends Equatable {
/// Payment term name (e.g., "Thanh toán hoàn toàn", "Thanh toán trả trước")
final String name;
/// Custom description (e.g., "Thanh toán ngay được chiết khấu 2%")
final String customDescription;
const PaymentTerm({
required this.name,
required this.customDescription,
});
@override
List<Object?> get props => [name, customDescription];
}

View File

@@ -0,0 +1,26 @@
/// Order Repository Interface
///
/// Defines the contract for order-related data operations.
library;
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 available order statuses
Future<List<OrderStatus>> getOrderStatusList();
/// Get list of available payment terms
Future<List<PaymentTerm>> getPaymentTermsList();
/// Create new order
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,
});
}

View File

@@ -0,0 +1,280 @@
/// Order Success Page
///
/// Displays order confirmation after successful order placement.
/// Features:
/// - Success icon and message
/// - Order information (order number, date, total, payment method, status)
/// - Different message for negotiation requests
/// - Navigation to order details or home
library;
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.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/router/app_router.dart';
import 'package:worker/core/theme/colors.dart';
/// Order Success Page
class OrderSuccessPage extends StatelessWidget {
const OrderSuccessPage({
super.key,
required this.orderNumber,
this.total,
this.paymentMethod,
this.isNegotiation = false,
});
final String orderNumber;
final double? total;
final String? paymentMethod;
final bool isNegotiation;
@override
Widget build(BuildContext context) {
final now = DateTime.now();
final dateFormat = DateFormat('dd/MM/yyyy HH:mm');
return Scaffold(
backgroundColor: Colors.white,
body: SafeArea(
child: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.all(AppSpacing.lg),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Success Icon
Container(
width: 80,
height: 80,
decoration: BoxDecoration(
color: AppColors.success.withValues(alpha: 0.1),
shape: BoxShape.circle,
),
child: const Icon(
FontAwesomeIcons.check,
color: AppColors.success,
size: 40,
),
),
const SizedBox(height: AppSpacing.lg),
// Success Title
Text(
isNegotiation
? 'Gửi yêu cầu thành công!'
: 'Tạo đơn hàng thành công!',
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Color(0xFF212121),
),
textAlign: TextAlign.center,
),
const SizedBox(height: AppSpacing.sm),
// Success Message
Text(
isNegotiation
? 'Chúng tôi sẽ liên hệ với bạn để đàm phán giá trong vòng 24 giờ.'
: 'Cảm ơn bạn đã đặt hàng. Chúng tôi sẽ liên hệ xác nhận trong vòng 24 giờ.',
style: const TextStyle(
fontSize: 14,
color: AppColors.grey500,
),
textAlign: TextAlign.center,
),
const SizedBox(height: AppSpacing.xl),
// Order Info Card
Container(
width: double.infinity,
padding: const EdgeInsets.all(AppSpacing.md),
decoration: BoxDecoration(
color: const Color(0xFFF4F6F8),
borderRadius: BorderRadius.circular(AppRadius.card),
),
child: Column(
children: [
// Order Number
Column(
children: [
const Text(
'Mã đơn hàng',
style: TextStyle(
fontSize: 12,
color: AppColors.grey500,
),
),
const SizedBox(height: 4),
Text(
orderNumber,
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: AppColors.primaryBlue,
),
),
],
),
const SizedBox(height: AppSpacing.md),
// Order Date
_buildInfoRow(
'Ngày đặt',
dateFormat.format(now),
),
const SizedBox(height: AppSpacing.sm),
// Total Amount
if (total != null)
_buildInfoRow(
'Tổng tiền',
_formatCurrency(total!),
valueStyle: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: Color(0xFF212121),
),
),
if (total != null) const SizedBox(height: AppSpacing.sm),
// Payment Method
if (paymentMethod != null && !isNegotiation)
_buildInfoRow(
'Phương thức thanh toán',
paymentMethod!,
),
if (paymentMethod != null && !isNegotiation)
const SizedBox(height: AppSpacing.sm),
// Status
_buildInfoRow(
'Trạng thái',
isNegotiation ? 'Chờ đàm phán' : 'Chờ xác nhận',
valueStyle: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: isNegotiation
? AppColors.warning
: AppColors.warning,
),
),
],
),
),
const SizedBox(height: AppSpacing.xl),
// View Order Details Button
SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
onPressed: () {
// Navigate to order details page
context.pushReplacementNamed(
RouteNames.orderDetail,
pathParameters: {'orderId': orderNumber},
);
},
icon: const FaIcon(FontAwesomeIcons.eye, size: 18),
label: const Text(
'Xem chi tiết đơn hàng',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primaryBlue,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 16),
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppRadius.button),
),
),
),
),
const SizedBox(height: AppSpacing.sm),
// Back to Home Button
SizedBox(
width: double.infinity,
child: OutlinedButton.icon(
onPressed: () {
// Navigate to home page
context.goNamed(RouteNames.home);
},
icon: const FaIcon(FontAwesomeIcons.house, size: 18),
label: const Text(
'Quay về trang chủ',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
style: OutlinedButton.styleFrom(
foregroundColor: AppColors.grey900,
side: BorderSide(
color: AppColors.grey100,
width: 1.5,
),
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppRadius.button),
),
),
),
),
],
),
),
),
),
);
}
/// Build info row
Widget _buildInfoRow(
String label,
String value, {
TextStyle? valueStyle,
}) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
label,
style: const TextStyle(
fontSize: 14,
color: AppColors.grey500,
),
),
Text(
value,
style: valueStyle ??
const TextStyle(
fontSize: 14,
color: Color(0xFF212121),
),
),
],
);
}
/// Format currency
String _formatCurrency(double amount) {
return '${amount.toStringAsFixed(0).replaceAllMapped(RegExp(r'(\d)(?=(\d{3})+(?!\d))'), (Match m) => '${m[1]}.')}';
}
}

View File

@@ -0,0 +1,26 @@
/// Order Data Providers
///
/// Riverpod providers for order data sources and repositories.
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/repositories/order_repository_impl.dart';
import 'package:worker/features/orders/domain/repositories/order_repository.dart';
part 'order_data_providers.g.dart';
/// Provider for Order Remote Data Source
@riverpod
Future<OrderRemoteDataSource> orderRemoteDataSource(Ref ref) async {
final dioClient = await ref.watch(dioClientProvider.future);
return OrderRemoteDataSource(dioClient);
}
/// Provider for Order Repository
@riverpod
Future<OrderRepository> orderRepository(Ref ref) async {
final remoteDataSource = await ref.watch(orderRemoteDataSourceProvider.future);
return OrderRepositoryImpl(remoteDataSource);
}

View File

@@ -0,0 +1,100 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'order_data_providers.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
/// Provider for Order Remote Data Source
@ProviderFor(orderRemoteDataSource)
const orderRemoteDataSourceProvider = OrderRemoteDataSourceProvider._();
/// Provider for Order Remote Data Source
final class OrderRemoteDataSourceProvider
extends
$FunctionalProvider<
AsyncValue<OrderRemoteDataSource>,
OrderRemoteDataSource,
FutureOr<OrderRemoteDataSource>
>
with
$FutureModifier<OrderRemoteDataSource>,
$FutureProvider<OrderRemoteDataSource> {
/// Provider for Order Remote Data Source
const OrderRemoteDataSourceProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'orderRemoteDataSourceProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$orderRemoteDataSourceHash();
@$internal
@override
$FutureProviderElement<OrderRemoteDataSource> $createElement(
$ProviderPointer pointer,
) => $FutureProviderElement(pointer);
@override
FutureOr<OrderRemoteDataSource> create(Ref ref) {
return orderRemoteDataSource(ref);
}
}
String _$orderRemoteDataSourceHash() =>
r'f4e14afbd8ae9e4348cba8f9a983ff9cb5e2a4c7';
/// Provider for Order Repository
@ProviderFor(orderRepository)
const orderRepositoryProvider = OrderRepositoryProvider._();
/// Provider for Order Repository
final class OrderRepositoryProvider
extends
$FunctionalProvider<
AsyncValue<OrderRepository>,
OrderRepository,
FutureOr<OrderRepository>
>
with $FutureModifier<OrderRepository>, $FutureProvider<OrderRepository> {
/// Provider for Order Repository
const OrderRepositoryProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'orderRepositoryProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$orderRepositoryHash();
@$internal
@override
$FutureProviderElement<OrderRepository> $createElement(
$ProviderPointer pointer,
) => $FutureProviderElement(pointer);
@override
FutureOr<OrderRepository> create(Ref ref) {
return orderRepository(ref);
}
}
String _$orderRepositoryHash() => r'985408a6667ab31427524f9b1981287c28f4f221';

View File

@@ -0,0 +1,44 @@
/// Order Repository Provider
///
/// Provides the order repository instance and related providers.
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/repositories/order_repository_impl.dart';
import 'package:worker/features/orders/domain/repositories/order_repository.dart';
part 'order_repository_provider.g.dart';
/// Order Repository Provider
@riverpod
Future<OrderRepository> orderRepository(Ref ref) async {
final dioClient = await ref.watch(dioClientProvider.future);
final remoteDataSource = OrderRemoteDataSource(dioClient);
return OrderRepositoryImpl(remoteDataSource);
}
/// Create Order Provider
///
/// Creates a new order with the given parameters.
@riverpod
Future<Map<String, dynamic>> createOrder(
Ref ref, {
required List<Map<String, dynamic>> items,
required Map<String, dynamic> deliveryAddress,
required String paymentMethod,
bool needsInvoice = false,
bool needsNegotiation = false,
String? notes,
}) async {
final repository = await ref.watch(orderRepositoryProvider.future);
return await repository.createOrder(
items: items,
deliveryAddress: deliveryAddress,
paymentMethod: paymentMethod,
needsInvoice: needsInvoice,
needsNegotiation: needsNegotiation,
notes: notes,
);
}

View File

@@ -0,0 +1,201 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'order_repository_provider.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
/// Order Repository Provider
@ProviderFor(orderRepository)
const orderRepositoryProvider = OrderRepositoryProvider._();
/// Order Repository Provider
final class OrderRepositoryProvider
extends
$FunctionalProvider<
AsyncValue<OrderRepository>,
OrderRepository,
FutureOr<OrderRepository>
>
with $FutureModifier<OrderRepository>, $FutureProvider<OrderRepository> {
/// Order Repository Provider
const OrderRepositoryProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'orderRepositoryProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$orderRepositoryHash();
@$internal
@override
$FutureProviderElement<OrderRepository> $createElement(
$ProviderPointer pointer,
) => $FutureProviderElement(pointer);
@override
FutureOr<OrderRepository> create(Ref ref) {
return orderRepository(ref);
}
}
String _$orderRepositoryHash() => r'15efafcf3b545ea52fdc8d0acbd8192ba8f41546';
/// Create Order Provider
///
/// Creates a new order with the given parameters.
@ProviderFor(createOrder)
const createOrderProvider = CreateOrderFamily._();
/// Create Order Provider
///
/// Creates a new order with the given parameters.
final class CreateOrderProvider
extends
$FunctionalProvider<
AsyncValue<Map<String, dynamic>>,
Map<String, dynamic>,
FutureOr<Map<String, dynamic>>
>
with
$FutureModifier<Map<String, dynamic>>,
$FutureProvider<Map<String, dynamic>> {
/// Create Order Provider
///
/// Creates a new order with the given parameters.
const CreateOrderProvider._({
required CreateOrderFamily super.from,
required ({
List<Map<String, dynamic>> items,
Map<String, dynamic> deliveryAddress,
String paymentMethod,
bool needsInvoice,
bool needsNegotiation,
String? notes,
})
super.argument,
}) : super(
retry: null,
name: r'createOrderProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$createOrderHash();
@override
String toString() {
return r'createOrderProvider'
''
'$argument';
}
@$internal
@override
$FutureProviderElement<Map<String, dynamic>> $createElement(
$ProviderPointer pointer,
) => $FutureProviderElement(pointer);
@override
FutureOr<Map<String, dynamic>> create(Ref ref) {
final argument =
this.argument
as ({
List<Map<String, dynamic>> items,
Map<String, dynamic> deliveryAddress,
String paymentMethod,
bool needsInvoice,
bool needsNegotiation,
String? notes,
});
return createOrder(
ref,
items: argument.items,
deliveryAddress: argument.deliveryAddress,
paymentMethod: argument.paymentMethod,
needsInvoice: argument.needsInvoice,
needsNegotiation: argument.needsNegotiation,
notes: argument.notes,
);
}
@override
bool operator ==(Object other) {
return other is CreateOrderProvider && other.argument == argument;
}
@override
int get hashCode {
return argument.hashCode;
}
}
String _$createOrderHash() => r'2d13526815e19a2bbef2f2974dad991d8ffcb594';
/// Create Order Provider
///
/// Creates a new order with the given parameters.
final class CreateOrderFamily extends $Family
with
$FunctionalFamilyOverride<
FutureOr<Map<String, dynamic>>,
({
List<Map<String, dynamic>> items,
Map<String, dynamic> deliveryAddress,
String paymentMethod,
bool needsInvoice,
bool needsNegotiation,
String? notes,
})
> {
const CreateOrderFamily._()
: super(
retry: null,
name: r'createOrderProvider',
dependencies: null,
$allTransitiveDependencies: null,
isAutoDispose: true,
);
/// Create Order Provider
///
/// Creates a new order with the given parameters.
CreateOrderProvider call({
required List<Map<String, dynamic>> items,
required Map<String, dynamic> deliveryAddress,
required String paymentMethod,
bool needsInvoice = false,
bool needsNegotiation = false,
String? notes,
}) => CreateOrderProvider._(
argument: (
items: items,
deliveryAddress: deliveryAddress,
paymentMethod: paymentMethod,
needsInvoice: needsInvoice,
needsNegotiation: needsNegotiation,
notes: notes,
),
from: this,
);
@override
String toString() => r'createOrderProvider';
}

View File

@@ -0,0 +1,20 @@
/// Order Status Provider
///
/// Provides order status list from the API.
library;
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:worker/features/orders/domain/entities/order_status.dart';
import 'package:worker/features/orders/presentation/providers/order_data_providers.dart';
part 'order_status_provider.g.dart';
/// Provider for fetching order status list
///
/// This provider automatically fetches the list when accessed.
/// Returns AsyncValue<List<OrderStatus>> which handles loading/error states.
@riverpod
Future<List<OrderStatus>> orderStatusList(Ref ref) async {
final repository = await ref.watch(orderRepositoryProvider.future);
return repository.getOrderStatusList();
}

View File

@@ -0,0 +1,64 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'order_status_provider.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
/// Provider for fetching order status list
///
/// This provider automatically fetches the list when accessed.
/// Returns AsyncValue<List<OrderStatus>> which handles loading/error states.
@ProviderFor(orderStatusList)
const orderStatusListProvider = OrderStatusListProvider._();
/// Provider for fetching order status list
///
/// This provider automatically fetches the list when accessed.
/// Returns AsyncValue<List<OrderStatus>> which handles loading/error states.
final class OrderStatusListProvider
extends
$FunctionalProvider<
AsyncValue<List<OrderStatus>>,
List<OrderStatus>,
FutureOr<List<OrderStatus>>
>
with
$FutureModifier<List<OrderStatus>>,
$FutureProvider<List<OrderStatus>> {
/// Provider for fetching order status list
///
/// This provider automatically fetches the list when accessed.
/// Returns AsyncValue<List<OrderStatus>> which handles loading/error states.
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'feb0d93e57f22e0c39c34e0a655c0972d874904e';

View File

@@ -0,0 +1,20 @@
/// Payment Terms Provider
///
/// Provides payment terms list from the API.
library;
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:worker/features/orders/domain/entities/payment_term.dart';
import 'package:worker/features/orders/presentation/providers/order_data_providers.dart';
part 'payment_terms_provider.g.dart';
/// Provider for fetching payment terms list
///
/// This provider automatically fetches the list when accessed.
/// Returns AsyncValue<List<PaymentTerm>> which handles loading/error states.
@riverpod
Future<List<PaymentTerm>> paymentTermsList(Ref ref) async {
final repository = await ref.watch(orderRepositoryProvider.future);
return repository.getPaymentTermsList();
}

View File

@@ -0,0 +1,64 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'payment_terms_provider.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
/// Provider for fetching payment terms list
///
/// This provider automatically fetches the list when accessed.
/// Returns AsyncValue<List<PaymentTerm>> which handles loading/error states.
@ProviderFor(paymentTermsList)
const paymentTermsListProvider = PaymentTermsListProvider._();
/// Provider for fetching payment terms list
///
/// This provider automatically fetches the list when accessed.
/// Returns AsyncValue<List<PaymentTerm>> which handles loading/error states.
final class PaymentTermsListProvider
extends
$FunctionalProvider<
AsyncValue<List<PaymentTerm>>,
List<PaymentTerm>,
FutureOr<List<PaymentTerm>>
>
with
$FutureModifier<List<PaymentTerm>>,
$FutureProvider<List<PaymentTerm>> {
/// Provider for fetching payment terms list
///
/// This provider automatically fetches the list when accessed.
/// Returns AsyncValue<List<PaymentTerm>> which handles loading/error states.
const PaymentTermsListProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'paymentTermsListProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$paymentTermsListHash();
@$internal
@override
$FutureProviderElement<List<PaymentTerm>> $createElement(
$ProviderPointer pointer,
) => $FutureProviderElement(pointer);
@override
FutureOr<List<PaymentTerm>> create(Ref ref) {
return paymentTermsList(ref);
}
}
String _$paymentTermsListHash() => r'6074016c04d947058b731c334b8f84fe85c36124';

View File

@@ -6,7 +6,6 @@ library;
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:intl/intl.dart';
import 'package:worker/core/database/models/enums.dart';
import 'package:worker/core/theme/colors.dart';

View File

@@ -66,6 +66,7 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
// Show feedback
final isFavorite = ref.read(isFavoriteProvider(widget.productId));
if (mounted) {
ScaffoldMessenger.of(context).clearSnackBars();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
@@ -117,6 +118,7 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
title: const Text('Chia sẻ qua tin nhắn'),
onTap: () {
Navigator.pop(context);
ScaffoldMessenger.of(context).clearSnackBars();
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Đang phát triển tính năng chat'),

View File

@@ -34,7 +34,6 @@ class ProductsPage extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final l10n = AppLocalizations.of(context);
final productsAsync = ref.watch(productsProvider);
final cartItemCount = ref.watch(cartItemCountProvider);
// Preload filter options for better UX when opening filter drawer
ref.watch(productFilterOptionsProvider);
@@ -53,8 +52,11 @@ class ProductsPage extends ConsumerWidget {
foregroundColor: AppColors.grey900,
centerTitle: false,
actions: [
// Cart Icon with Badge
IconButton(
// Cart Icon with Badge (extracted to Consumer to prevent full page rebuild)
Consumer(
builder: (context, ref, child) {
final cartItemCount = ref.watch(cartItemCountProvider);
return IconButton(
icon: Badge(
label: Text('$cartItemCount'),
backgroundColor: AppColors.danger,
@@ -63,6 +65,8 @@ class ProductsPage extends ConsumerWidget {
child: const FaIcon(FontAwesomeIcons.cartShopping, color: Colors.black, size: 20),
),
onPressed: () => context.push(RouteNames.cart),
);
},
),
const SizedBox(width: AppSpacing.sm),
],
@@ -129,13 +133,29 @@ class ProductsPage extends ConsumerWidget {
// Add to cart
ref.read(cartProvider.notifier).addToCart(product);
ScaffoldMessenger.of(context).showSnackBar(
// Show SnackBar with manual dismissal
final messenger = ScaffoldMessenger.of(context)
..clearSnackBars();
final controller = messenger.showSnackBar(
SnackBar(
content: Text('${product.name} đã thêm vào giỏ hàng'),
duration: const Duration(seconds: 2),
action: SnackBarAction(label: 'Xem', onPressed: () => context.go(RouteNames.cart)),
duration: const Duration(days: 365), // Prevent auto-dismiss
behavior: SnackBarBehavior.floating,
action: SnackBarAction(
label: 'Xem',
onPressed: () {
messenger.hideCurrentSnackBar();
context.go(RouteNames.cart);
},
),
),
);
// Manually dismiss after 2 seconds
Future.delayed(const Duration(seconds: 2), () {
controller.close();
});
},
);
},