Compare commits
3 Commits
dc85157758
...
06b0834822
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
06b0834822 | ||
|
|
4913a4e04b | ||
|
|
f2f95849d4 |
118
docs/order.sh
Normal file
118
docs/order.sh
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
|
||||||
|
#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á
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}'
|
||||||
|
|
||||||
|
#create order response
|
||||||
|
Response: {message: {success: true, message: Sales Order created successfully, data: {name: SAL-ORD-2025-00078, status_color: Warning, status: Chờ phê duyệt, grand_total: 589824.0}}}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#gen qrcode
|
||||||
|
curl --location 'https://land.dbiz.com//api/method/building_material.building_material.api.v1.qrcode.generate' \
|
||||||
|
--header 'X-Frappe-Csrf-Token: 6ff3be4d1f887dbebf86ba4502b05d94b30c0b0569de49b74a7171a9' \
|
||||||
|
--header 'Cookie: sid=a0cbe3ea6f9a7e9cf083bbe3139eada68d2357eac0167bcc66cda17d; sid=a0cbe3ea6f9a7e9cf083bbe3139eada68d2357eac0167bcc66cda17d' \
|
||||||
|
--header 'Content-Type: application/json' \
|
||||||
|
--data '{
|
||||||
|
"order_id" : "SAL-ORD-2025-00048"
|
||||||
|
}'
|
||||||
|
|
||||||
|
#gen qrcode response
|
||||||
|
{
|
||||||
|
"message": {
|
||||||
|
"qr_code": "00020101021238540010A00000072701240006970422011008490428160208QRIBFTTA53037045802VN62220818SAL-ORD-2025-00048630430F4",
|
||||||
|
"amount": null,
|
||||||
|
"transaction_id": "SAL-ORD-2025-00048",
|
||||||
|
"bank_info": {
|
||||||
|
"bank_name": "MB Bank",
|
||||||
|
"account_no": "0849042816",
|
||||||
|
"account_name": "NGUYEN MINH CHAU"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
<i class="fas fa-check"></i>
|
<i class="fas fa-check"></i>
|
||||||
</div>
|
</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">
|
<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ờ.
|
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>
|
</p>
|
||||||
@@ -46,7 +46,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Next Steps -->
|
<!-- Next Steps -->
|
||||||
<div class="card">
|
<!-- <div class="card">
|
||||||
<h3 class="card-title">Các bước tiếp theo</h3>
|
<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="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>
|
<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>
|
<p class="text-small text-muted">Vận chuyển đến địa chỉ của bạn</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>-->
|
||||||
|
|
||||||
<!-- Action Buttons -->
|
<!-- Action Buttons -->
|
||||||
<a href="#" class="btn btn-primary btn-block mb-2">
|
<a href="#" class="btn btn-primary btn-block mb-2">
|
||||||
|
|||||||
@@ -122,7 +122,7 @@
|
|||||||
|
|
||||||
<div class="info-row">
|
<div class="info-row">
|
||||||
<span class="info-label">Nội dung:</span>
|
<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')">
|
<button class="copy-btn" onclick="copyText('DH001234 La Nguyen Quynh')">
|
||||||
<i class="fas fa-copy"></i>
|
<i class="fas fa-copy"></i>
|
||||||
</button>
|
</button>
|
||||||
@@ -139,12 +139,15 @@
|
|||||||
|
|
||||||
<!-- Action Buttons -->
|
<!-- Action Buttons -->
|
||||||
<div class="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
|
<i class="fas fa-check"></i> Đã thanh toán
|
||||||
</button>
|
</button>-->
|
||||||
<button class="btn btn-primary" onclick="uploadProof()">
|
<button class="btn btn-primary" onclick="uploadProof()">
|
||||||
<i class="fas fa-camera"></i> Upload bill chuyển khoản
|
<i class="fas fa-camera"></i> Upload bill chuyển khoản
|
||||||
</button>
|
</button>
|
||||||
|
<a href="index.html" class="btn btn-secondary btn-block">
|
||||||
|
<i class="fas fa-home"></i> Quay về trang chủ
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Timer -->
|
<!-- Timer -->
|
||||||
|
|||||||
@@ -98,9 +98,9 @@ PODS:
|
|||||||
- Flutter
|
- Flutter
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- PromisesObjC (2.4.0)
|
- PromisesObjC (2.4.0)
|
||||||
- SDWebImage (5.21.2):
|
- SDWebImage (5.21.3):
|
||||||
- SDWebImage/Core (= 5.21.2)
|
- SDWebImage/Core (= 5.21.3)
|
||||||
- SDWebImage/Core (5.21.2)
|
- SDWebImage/Core (5.21.3)
|
||||||
- share_plus (0.0.1):
|
- share_plus (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- shared_preferences_foundation (0.0.1):
|
- shared_preferences_foundation (0.0.1):
|
||||||
@@ -195,7 +195,7 @@ SPEC CHECKSUMS:
|
|||||||
nanopb: 438bc412db1928dac798aa6fd75726007be04262
|
nanopb: 438bc412db1928dac798aa6fd75726007be04262
|
||||||
path_provider_foundation: 0b743cbb62d8e47eab856f09262bb8c1ddcfe6ba
|
path_provider_foundation: 0b743cbb62d8e47eab856f09262bb8c1ddcfe6ba
|
||||||
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
||||||
SDWebImage: 9f177d83116802728e122410fb25ad88f5c7608a
|
SDWebImage: 16309af6d214ba3f77a7c6f6fdda888cb313a50a
|
||||||
share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f
|
share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f
|
||||||
shared_preferences_foundation: 5086985c1d43c5ba4d5e69a4e8083a389e2909e6
|
shared_preferences_foundation: 5086985c1d43c5ba4d5e69a4e8083a389e2909e6
|
||||||
sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
|
sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
|
||||||
|
|||||||
@@ -211,10 +211,21 @@ class ApiConstants {
|
|||||||
// Order Endpoints
|
// Order Endpoints
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
/// Create new order
|
/// Get order status list (requires sid and csrf_token)
|
||||||
/// POST /orders
|
/// POST /api/method/building_material.building_material.api.sales_order.get_order_status_list
|
||||||
/// Body: { "items": [...], "deliveryAddress": {...}, "paymentMethod": "..." }
|
/// Returns: { "message": [{ "status": "...", "label": "...", "color": "...", "index": 0 }] }
|
||||||
static const String createOrder = '/orders';
|
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';
|
||||||
|
|
||||||
|
/// Generate QR code for payment (requires sid and csrf_token)
|
||||||
|
/// POST /api/method/building_material.building_material.api.v1.qrcode.generate
|
||||||
|
/// Body: { "order_id": "SAL-ORD-2025-00048" }
|
||||||
|
/// 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
|
/// Get user's orders
|
||||||
/// GET /orders?status={status}&page={page}&limit={limit}
|
/// GET /orders?status={status}&page={page}&limit={limit}
|
||||||
|
|||||||
@@ -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_detail_page.dart';
|
||||||
import 'package:worker/features/news/presentation/pages/news_list_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_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/orders_page.dart';
|
||||||
import 'package:worker/features/orders/presentation/pages/payment_detail_page.dart';
|
import 'package:worker/features/orders/presentation/pages/payment_detail_page.dart';
|
||||||
import 'package:worker/features/orders/presentation/pages/payment_qr_page.dart';
|
import 'package:worker/features/orders/presentation/pages/payment_qr_page.dart';
|
||||||
@@ -322,7 +323,33 @@ final routerProvider = Provider<GoRouter>((ref) {
|
|||||||
final amount = double.tryParse(amountStr) ?? 0.0;
|
final amount = double.tryParse(amountStr) ?? 0.0;
|
||||||
return MaterialPage(
|
return MaterialPage(
|
||||||
key: state.pageKey,
|
key: state.pageKey,
|
||||||
child: PaymentQrPage(orderId: orderId, amount: amount),
|
child: PaymentQrPage(
|
||||||
|
orderId: orderId,
|
||||||
|
amount: amount,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ class _LoginPageState extends ConsumerState<LoginPage> {
|
|||||||
final _formKey = GlobalKey<FormState>();
|
final _formKey = GlobalKey<FormState>();
|
||||||
|
|
||||||
// Controllers
|
// Controllers
|
||||||
final _phoneController = TextEditingController(text: "0978113710");
|
final _phoneController = TextEditingController(text: "0986788766");
|
||||||
final _passwordController = TextEditingController(text: "123456");
|
final _passwordController = TextEditingController(text: "123456");
|
||||||
|
|
||||||
// Focus nodes
|
// Focus nodes
|
||||||
|
|||||||
@@ -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/order_summary_section.dart';
|
||||||
import 'package:worker/features/cart/presentation/widgets/payment_method_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/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
|
/// Checkout Page
|
||||||
///
|
///
|
||||||
@@ -51,12 +53,15 @@ class CheckoutPage extends HookConsumerWidget {
|
|||||||
// Invoice section
|
// Invoice section
|
||||||
final needsInvoice = useState<bool>(false);
|
final needsInvoice = useState<bool>(false);
|
||||||
|
|
||||||
// Payment method
|
// Payment method (will be set to first payment term name from API)
|
||||||
final paymentMethod = useState<String>('full_payment');
|
final paymentMethod = useState<String>('');
|
||||||
|
|
||||||
// Price negotiation
|
// Price negotiation
|
||||||
final needsNegotiation = useState<bool>(false);
|
final needsNegotiation = useState<bool>(false);
|
||||||
|
|
||||||
|
// Watch API provider for payment terms
|
||||||
|
final paymentTermsListAsync = ref.watch(paymentTermsListProvider);
|
||||||
|
|
||||||
// Get CartItemData from navigation
|
// Get CartItemData from navigation
|
||||||
final cartItemsData = checkoutData?['cartItems'] as List<dynamic>? ?? [];
|
final cartItemsData = checkoutData?['cartItems'] as List<dynamic>? ?? [];
|
||||||
|
|
||||||
@@ -85,7 +90,7 @@ class CheckoutPage extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// TODO: Fetch member discount from user profile API
|
// 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);
|
final memberDiscount = subtotal * (memberDiscountPercent / 100);
|
||||||
|
|
||||||
// TODO: Fetch shipping fee from API based on address
|
// 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)
|
// Payment Method Section (hidden if negotiation is checked)
|
||||||
if (!needsNegotiation.value)
|
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)
|
if (!needsNegotiation.value)
|
||||||
const SizedBox(height: AppSpacing.md),
|
const SizedBox(height: AppSpacing.md),
|
||||||
@@ -164,6 +240,7 @@ class CheckoutPage extends HookConsumerWidget {
|
|||||||
// Price Negotiation Section
|
// Price Negotiation Section
|
||||||
PriceNegotiationSection(needsNegotiation: needsNegotiation),
|
PriceNegotiationSection(needsNegotiation: needsNegotiation),
|
||||||
|
|
||||||
|
|
||||||
const SizedBox(height: AppSpacing.md),
|
const SizedBox(height: AppSpacing.md),
|
||||||
|
|
||||||
// Terms and Conditions
|
// Terms and Conditions
|
||||||
@@ -200,6 +277,8 @@ class CheckoutPage extends HookConsumerWidget {
|
|||||||
selectedAddress: selectedAddress.value,
|
selectedAddress: selectedAddress.value,
|
||||||
paymentMethod: paymentMethod.value,
|
paymentMethod: paymentMethod.value,
|
||||||
total: total,
|
total: total,
|
||||||
|
cartItems: checkoutItems,
|
||||||
|
notes: notesController.text.trim().isEmpty ? null : notesController.text.trim(),
|
||||||
),
|
),
|
||||||
|
|
||||||
const SizedBox(height: AppSpacing.lg),
|
const SizedBox(height: AppSpacing.lg),
|
||||||
|
|||||||
@@ -8,11 +8,7 @@ import 'package:worker/features/products/domain/entities/product.dart';
|
|||||||
/// Cart Item Data
|
/// Cart Item Data
|
||||||
///
|
///
|
||||||
/// Represents a product in the cart with quantity.
|
/// Represents a product in the cart with quantity.
|
||||||
class CartItemData {
|
class CartItemData { // Number of tiles/boxes needed
|
||||||
final Product product;
|
|
||||||
final double quantity;
|
|
||||||
final double quantityConverted; // Rounded-up quantity for actual billing
|
|
||||||
final int boxes; // Number of tiles/boxes needed
|
|
||||||
|
|
||||||
const CartItemData({
|
const CartItemData({
|
||||||
required this.product,
|
required this.product,
|
||||||
@@ -20,6 +16,10 @@ class CartItemData {
|
|||||||
required this.quantityConverted,
|
required this.quantityConverted,
|
||||||
required this.boxes,
|
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)
|
/// Calculate line total using CONVERTED quantity (important for accurate billing)
|
||||||
double get lineTotal => product.basePrice * quantityConverted;
|
double get lineTotal => product.basePrice * quantityConverted;
|
||||||
@@ -43,19 +43,6 @@ class CartItemData {
|
|||||||
///
|
///
|
||||||
/// Represents the complete state of the shopping cart.
|
/// Represents the complete state of the shopping cart.
|
||||||
class CartState {
|
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({
|
const CartState({
|
||||||
required this.items,
|
required this.items,
|
||||||
@@ -88,6 +75,19 @@ class CartState {
|
|||||||
total: 0.0,
|
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 isEmpty => items.isEmpty;
|
||||||
bool get isNotEmpty => items.isNotEmpty;
|
bool get isNotEmpty => items.isNotEmpty;
|
||||||
|
|||||||
@@ -5,16 +5,18 @@ library;
|
|||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:go_router/go_router.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/constants/ui_constants.dart';
|
||||||
import 'package:worker/core/router/app_router.dart';
|
import 'package:worker/core/router/app_router.dart';
|
||||||
import 'package:worker/core/theme/colors.dart';
|
import 'package:worker/core/theme/colors.dart';
|
||||||
import 'package:worker/features/account/domain/entities/address.dart';
|
import 'package:worker/features/account/domain/entities/address.dart';
|
||||||
|
import 'package:worker/features/orders/presentation/providers/order_repository_provider.dart';
|
||||||
|
|
||||||
/// Checkout Submit Button
|
/// Checkout Submit Button
|
||||||
///
|
///
|
||||||
/// Button that changes based on negotiation checkbox state.
|
/// Button that changes based on negotiation checkbox state.
|
||||||
class CheckoutSubmitButton extends StatelessWidget {
|
class CheckoutSubmitButton extends HookConsumerWidget {
|
||||||
const CheckoutSubmitButton({
|
const CheckoutSubmitButton({
|
||||||
super.key,
|
super.key,
|
||||||
required this.formKey,
|
required this.formKey,
|
||||||
@@ -23,6 +25,8 @@ class CheckoutSubmitButton extends StatelessWidget {
|
|||||||
required this.selectedAddress,
|
required this.selectedAddress,
|
||||||
required this.paymentMethod,
|
required this.paymentMethod,
|
||||||
required this.total,
|
required this.total,
|
||||||
|
required this.cartItems,
|
||||||
|
this.notes,
|
||||||
});
|
});
|
||||||
|
|
||||||
final GlobalKey<FormState> formKey;
|
final GlobalKey<FormState> formKey;
|
||||||
@@ -31,9 +35,11 @@ class CheckoutSubmitButton extends StatelessWidget {
|
|||||||
final Address? selectedAddress;
|
final Address? selectedAddress;
|
||||||
final String paymentMethod;
|
final String paymentMethod;
|
||||||
final double total;
|
final double total;
|
||||||
|
final List<Map<String, dynamic>> cartItems;
|
||||||
|
final String? notes;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
return Container(
|
return Container(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
padding: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
|
padding: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
|
||||||
@@ -52,7 +58,7 @@ class CheckoutSubmitButton extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (formKey.currentState?.validate() ?? false) {
|
if (formKey.currentState?.validate() ?? false) {
|
||||||
_handlePlaceOrder(context);
|
_handlePlaceOrder(context, ref);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
@@ -78,48 +84,99 @@ class CheckoutSubmitButton extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Handle place order
|
/// Handle place order
|
||||||
void _handlePlaceOrder(BuildContext context) {
|
Future<void> _handlePlaceOrder(BuildContext context, WidgetRef ref) async {
|
||||||
// TODO: Implement actual order placement with backend
|
// Show loading indicator
|
||||||
|
showDialog<void>(
|
||||||
if (needsNegotiation) {
|
context: context,
|
||||||
// Show negotiation request sent message
|
barrierDismissible: false,
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
builder: (context) => const Center(
|
||||||
const SnackBar(
|
child: CircularProgressIndicator(),
|
||||||
content: Text('Yêu cầu đàm phán giá đã được gửi!'),
|
|
||||||
backgroundColor: AppColors.success,
|
|
||||||
duration: Duration(seconds: 2),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Navigate back after a short delay
|
try {
|
||||||
Future.delayed(const Duration(milliseconds: 500), () {
|
// Prepare delivery address data
|
||||||
if (context.mounted) {
|
final deliveryAddressData = {
|
||||||
context.pop();
|
'name': selectedAddress!.name,
|
||||||
}
|
'phone': selectedAddress!.phone,
|
||||||
});
|
'street': selectedAddress!.addressLine1,
|
||||||
} else {
|
'ward': selectedAddress!.wardName ?? selectedAddress!.wardCode,
|
||||||
// Generate order ID (mock - replace with actual from backend)
|
'city': selectedAddress!.cityName ?? selectedAddress!.cityCode,
|
||||||
final orderId =
|
};
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
|
||||||
|
// Extract order number from response
|
||||||
|
final orderNumber = result['orderNumber'] as String? ??
|
||||||
|
result['orderId'] as String? ??
|
||||||
'DH${DateTime.now().millisecondsSinceEpoch.toString().substring(7)}';
|
'DH${DateTime.now().millisecondsSinceEpoch.toString().substring(7)}';
|
||||||
|
|
||||||
// Show order success message
|
if (needsNegotiation) {
|
||||||
|
// Navigate to order success page with negotiation flag
|
||||||
|
if (context.mounted) {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
context.pushReplacementNamed(
|
||||||
|
RouteNames.orderSuccess,
|
||||||
|
queryParameters: {
|
||||||
|
'orderNumber': orderNumber,
|
||||||
|
'total': total.toString(),
|
||||||
|
'isNegotiation': 'true',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Close loading dialog
|
||||||
|
if (context.mounted) {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Navigate to payment QR page (it will fetch QR code data itself)
|
||||||
|
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(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(
|
SnackBar(
|
||||||
content: Text('Đặt hàng thành công! Chuyển đến thanh toán...'),
|
content: Text('Lỗi khi tạo đơn hàng: ${e.toString()}'),
|
||||||
backgroundColor: AppColors.success,
|
backgroundColor: AppColors.danger,
|
||||||
duration: Duration(seconds: 1),
|
duration: const Duration(seconds: 3),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
}
|
||||||
// Navigate to payment QR page after a short delay
|
|
||||||
Future.delayed(const Duration(milliseconds: 500), () {
|
|
||||||
if (context.mounted) {
|
|
||||||
context.pushNamed(
|
|
||||||
RouteNames.paymentQr,
|
|
||||||
queryParameters: {'orderId': orderId, 'amount': total.toString()},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
/// Payment Method Section Widget
|
/// Payment Method Section Widget
|
||||||
///
|
///
|
||||||
/// Payment method selection with two options:
|
/// Payment method selection with dynamic options from API.
|
||||||
/// 1. Full payment via bank transfer
|
|
||||||
/// 2. Partial payment (>=20%, 30 day terms)
|
|
||||||
library;
|
library;
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
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:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
import 'package:worker/core/constants/ui_constants.dart';
|
import 'package:worker/core/constants/ui_constants.dart';
|
||||||
import 'package:worker/core/theme/colors.dart';
|
import 'package:worker/core/theme/colors.dart';
|
||||||
|
import 'package:worker/features/orders/domain/entities/payment_term.dart';
|
||||||
|
|
||||||
/// Payment Method Section
|
/// Payment Method Section
|
||||||
///
|
///
|
||||||
/// Two payment options matching checkout.html design.
|
/// Displays payment options from API matching checkout.html design.
|
||||||
class PaymentMethodSection extends HookWidget {
|
class PaymentMethodSection extends HookWidget {
|
||||||
final ValueNotifier<String> paymentMethod;
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
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(
|
return Container(
|
||||||
margin: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
|
margin: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
|
||||||
padding: const EdgeInsets.all(AppSpacing.md),
|
padding: const EdgeInsets.all(AppSpacing.md),
|
||||||
@@ -50,15 +82,29 @@ class PaymentMethodSection extends HookWidget {
|
|||||||
|
|
||||||
const SizedBox(height: AppSpacing.md),
|
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(
|
InkWell(
|
||||||
onTap: () => paymentMethod.value = 'full_payment',
|
onTap: () => paymentMethod.value = term.name,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Radio<String>(
|
Radio<String>(
|
||||||
value: 'full_payment',
|
value: term.name,
|
||||||
groupValue: paymentMethod.value,
|
groupValue: paymentMethod.value,
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
paymentMethod.value = value!;
|
paymentMethod.value = value!;
|
||||||
@@ -66,27 +112,27 @@ class PaymentMethodSection extends HookWidget {
|
|||||||
activeColor: AppColors.primaryBlue,
|
activeColor: AppColors.primaryBlue,
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
const Icon(
|
Icon(
|
||||||
FontAwesomeIcons.buildingColumns,
|
icon,
|
||||||
color: AppColors.grey500,
|
color: AppColors.grey500,
|
||||||
size: 24,
|
size: 24,
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
const Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'Thanh toán hoàn toàn',
|
term.name,
|
||||||
style: TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
Text(
|
Text(
|
||||||
'Thanh toán qua tài khoản ngân hàng',
|
term.customDescription,
|
||||||
style: TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
color: AppColors.grey500,
|
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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
);
|
||||||
),
|
}),
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ class InvoicesLocalDataSource {
|
|||||||
throw Exception('Invalid JSON format: expected List');
|
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>))
|
.map((json) => InvoiceModel.fromJson(json as Map<String, dynamic>))
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,197 @@
|
|||||||
|
/// 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
|
||||||
|
// Response format: { message: { success: true, message: "...", data: { name: "SAL-ORD-2025-00078", ... } } }
|
||||||
|
final message = data['message'] as Map<String, dynamic>?;
|
||||||
|
if (message == null) {
|
||||||
|
throw Exception('No message field in createOrder response');
|
||||||
|
}
|
||||||
|
|
||||||
|
final orderData = message['data'] as Map<String, dynamic>?;
|
||||||
|
if (orderData == null) {
|
||||||
|
throw Exception('No data field in createOrder response');
|
||||||
|
}
|
||||||
|
|
||||||
|
final orderId = orderData['name'] as String?;
|
||||||
|
if (orderId == null || orderId.isEmpty) {
|
||||||
|
throw Exception('No order ID (name) in createOrder response');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return standardized response
|
||||||
|
return {
|
||||||
|
'orderId': orderId,
|
||||||
|
'orderNumber': orderId,
|
||||||
|
'fullResponse': orderData,
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
throw Exception('Failed to create order: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Generate QR code for payment
|
||||||
|
///
|
||||||
|
/// Calls: POST /api/method/building_material.building_material.api.v1.qrcode.generate
|
||||||
|
/// Body: { "order_id": "SAL-ORD-2025-00048" }
|
||||||
|
/// Returns: { "qr_code": "...", "amount": null, "transaction_id": "...", "bank_info": {...} }
|
||||||
|
Future<Map<String, dynamic>> generateQrCode(String orderId) async {
|
||||||
|
try {
|
||||||
|
final response = await _dioClient.post<Map<String, dynamic>>(
|
||||||
|
'${ApiConstants.frappeApiMethod}${ApiConstants.generateQrCode}',
|
||||||
|
data: {'order_id': orderId},
|
||||||
|
);
|
||||||
|
|
||||||
|
final data = response.data;
|
||||||
|
if (data == null) {
|
||||||
|
throw Exception('No data received from generateQrCode API');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract QR code info from Frappe response
|
||||||
|
final message = data['message'] as Map<String, dynamic>?;
|
||||||
|
if (message == null) {
|
||||||
|
throw Exception('No message field in generateQrCode response');
|
||||||
|
}
|
||||||
|
|
||||||
|
return message;
|
||||||
|
} catch (e) {
|
||||||
|
throw Exception('Failed to generate QR code: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,7 +24,7 @@ class OrdersLocalDataSource {
|
|||||||
throw Exception('Invalid JSON format: expected List');
|
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>))
|
.map((json) => OrderModel.fromJson(json as Map<String, dynamic>))
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
|
|||||||
65
lib/features/orders/data/models/order_status_model.dart
Normal file
65
lib/features/orders/data/models/order_status_model.dart
Normal 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];
|
||||||
|
}
|
||||||
53
lib/features/orders/data/models/payment_term_model.dart
Normal file
53
lib/features/orders/data/models/payment_term_model.dart
Normal 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];
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
/// 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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Map<String, dynamic>> generateQrCode(String orderId) async {
|
||||||
|
try {
|
||||||
|
return await _remoteDataSource.generateQrCode(orderId);
|
||||||
|
} catch (e) {
|
||||||
|
throw Exception('Failed to generate QR code: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
31
lib/features/orders/domain/entities/order_status.dart
Normal file
31
lib/features/orders/domain/entities/order_status.dart
Normal 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];
|
||||||
|
}
|
||||||
23
lib/features/orders/domain/entities/payment_term.dart
Normal file
23
lib/features/orders/domain/entities/payment_term.dart
Normal 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];
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
/// 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,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Generate QR code for payment
|
||||||
|
Future<Map<String, dynamic>> generateQrCode(String orderId);
|
||||||
|
}
|
||||||
280
lib/features/orders/presentation/pages/order_success_page.dart
Normal file
280
lib/features/orders/presentation/pages/order_success_page.dart
Normal 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]}.')}₫';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,9 +17,11 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
|||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:qr_flutter/qr_flutter.dart';
|
||||||
|
|
||||||
import 'package:worker/core/constants/ui_constants.dart';
|
import 'package:worker/core/constants/ui_constants.dart';
|
||||||
import 'package:worker/core/theme/colors.dart';
|
import 'package:worker/core/theme/colors.dart';
|
||||||
|
import 'package:worker/features/orders/presentation/providers/order_repository_provider.dart';
|
||||||
|
|
||||||
/// Payment QR Page
|
/// Payment QR Page
|
||||||
///
|
///
|
||||||
@@ -28,14 +30,43 @@ class PaymentQrPage extends HookConsumerWidget {
|
|||||||
final String orderId;
|
final String orderId;
|
||||||
final double amount;
|
final double amount;
|
||||||
|
|
||||||
const PaymentQrPage({super.key, required this.orderId, required this.amount});
|
const PaymentQrPage({
|
||||||
|
super.key,
|
||||||
|
required this.orderId,
|
||||||
|
required this.amount,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
// QR code data state
|
||||||
|
final qrCodeData = useState<Map<String, dynamic>?>(null);
|
||||||
|
final isLoadingQr = useState<bool>(true);
|
||||||
|
final qrError = useState<String?>(null);
|
||||||
|
|
||||||
// Countdown timer (15 minutes = 900 seconds)
|
// Countdown timer (15 minutes = 900 seconds)
|
||||||
final remainingSeconds = useState<int>(900);
|
final remainingSeconds = useState<int>(900);
|
||||||
final timer = useRef<Timer?>(null);
|
final timer = useRef<Timer?>(null);
|
||||||
|
|
||||||
|
// Fetch QR code data
|
||||||
|
useEffect(() {
|
||||||
|
Future<void> fetchQrCode() async {
|
||||||
|
try {
|
||||||
|
isLoadingQr.value = true;
|
||||||
|
qrError.value = null;
|
||||||
|
final repository = await ref.read(orderRepositoryProvider.future);
|
||||||
|
final data = await repository.generateQrCode(orderId);
|
||||||
|
qrCodeData.value = data;
|
||||||
|
} catch (e) {
|
||||||
|
qrError.value = e.toString();
|
||||||
|
} finally {
|
||||||
|
isLoadingQr.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchQrCode();
|
||||||
|
return null;
|
||||||
|
}, [orderId]);
|
||||||
|
|
||||||
// Start countdown timer
|
// Start countdown timer
|
||||||
useEffect(() {
|
useEffect(() {
|
||||||
timer.value = Timer.periodic(const Duration(seconds: 1), (t) {
|
timer.value = Timer.periodic(const Duration(seconds: 1), (t) {
|
||||||
@@ -94,12 +125,24 @@ class PaymentQrPage extends HookConsumerWidget {
|
|||||||
const SizedBox(height: AppSpacing.md),
|
const SizedBox(height: AppSpacing.md),
|
||||||
|
|
||||||
// QR Code Card
|
// QR Code Card
|
||||||
_buildQrCodeCard(amount, orderId),
|
_buildQrCodeCard(
|
||||||
|
amount,
|
||||||
|
orderId,
|
||||||
|
qrCodeData.value?['qr_code'] as String?,
|
||||||
|
isLoadingQr.value,
|
||||||
|
),
|
||||||
|
|
||||||
const SizedBox(height: AppSpacing.md),
|
const SizedBox(height: AppSpacing.md),
|
||||||
|
|
||||||
// Bank Transfer Info Card
|
// Bank Transfer Info Card
|
||||||
_buildBankInfoCard(context, orderId),
|
_buildBankInfoCard(
|
||||||
|
context,
|
||||||
|
orderId,
|
||||||
|
qrCodeData.value?['bank_info']?['bank_name'] as String?,
|
||||||
|
qrCodeData.value?['bank_info']?['account_no'] as String?,
|
||||||
|
qrCodeData.value?['bank_info']?['account_name'] as String?,
|
||||||
|
isLoadingQr.value,
|
||||||
|
),
|
||||||
|
|
||||||
const SizedBox(height: AppSpacing.md),
|
const SizedBox(height: AppSpacing.md),
|
||||||
|
|
||||||
@@ -180,14 +223,12 @@ class PaymentQrPage extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Build QR code card
|
/// Build QR code card
|
||||||
Widget _buildQrCodeCard(double amount, String orderId) {
|
Widget _buildQrCodeCard(
|
||||||
// Generate QR code data URL
|
double amount,
|
||||||
final qrData = Uri.encodeComponent(
|
String orderId,
|
||||||
'https://eurotile.com/payment/$orderId?amount=$amount',
|
String? qrCodeData,
|
||||||
);
|
bool isLoading,
|
||||||
final qrUrl =
|
) {
|
||||||
'https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=$qrData';
|
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
margin: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
|
margin: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
|
||||||
padding: const EdgeInsets.all(AppSpacing.md),
|
padding: const EdgeInsets.all(AppSpacing.md),
|
||||||
@@ -222,11 +263,19 @@ class PaymentQrPage extends HookConsumerWidget {
|
|||||||
borderRadius: BorderRadius.circular(AppRadius.card),
|
borderRadius: BorderRadius.circular(AppRadius.card),
|
||||||
border: Border.all(color: const Color(0xFFE2E8F0)),
|
border: Border.all(color: const Color(0xFFE2E8F0)),
|
||||||
),
|
),
|
||||||
child: Image.network(
|
child: isLoading
|
||||||
qrUrl,
|
? const Center(
|
||||||
fit: BoxFit.contain,
|
child: CircularProgressIndicator(),
|
||||||
errorBuilder: (context, error, stackTrace) {
|
)
|
||||||
return const Column(
|
: qrCodeData != null && qrCodeData.isNotEmpty
|
||||||
|
? QrImageView(
|
||||||
|
data: qrCodeData,
|
||||||
|
version: QrVersions.auto,
|
||||||
|
size: 200.0,
|
||||||
|
backgroundColor: Colors.white,
|
||||||
|
errorCorrectionLevel: QrErrorCorrectLevel.M,
|
||||||
|
)
|
||||||
|
: const Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
FaIcon(FontAwesomeIcons.qrcode, size: 80, color: AppColors.grey500),
|
FaIcon(FontAwesomeIcons.qrcode, size: 80, color: AppColors.grey500),
|
||||||
@@ -236,8 +285,6 @@ class PaymentQrPage extends HookConsumerWidget {
|
|||||||
style: TextStyle(fontSize: 12, color: AppColors.grey500),
|
style: TextStyle(fontSize: 12, color: AppColors.grey500),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: AppSpacing.md),
|
const SizedBox(height: AppSpacing.md),
|
||||||
@@ -252,7 +299,14 @@ class PaymentQrPage extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Build bank transfer info card
|
/// Build bank transfer info card
|
||||||
Widget _buildBankInfoCard(BuildContext context, String orderId) {
|
Widget _buildBankInfoCard(
|
||||||
|
BuildContext context,
|
||||||
|
String orderId,
|
||||||
|
String? bankName,
|
||||||
|
String? accountNo,
|
||||||
|
String? accountName,
|
||||||
|
bool isLoading,
|
||||||
|
) {
|
||||||
return Container(
|
return Container(
|
||||||
margin: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
|
margin: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
|
||||||
padding: const EdgeInsets.all(AppSpacing.md),
|
padding: const EdgeInsets.all(AppSpacing.md),
|
||||||
@@ -281,7 +335,11 @@ class PaymentQrPage extends HookConsumerWidget {
|
|||||||
const SizedBox(height: AppSpacing.md),
|
const SizedBox(height: AppSpacing.md),
|
||||||
|
|
||||||
// Bank Name
|
// Bank Name
|
||||||
_buildInfoRow(context: context, label: 'Ngân hàng:', value: 'BIDV'),
|
_buildInfoRow(
|
||||||
|
context: context,
|
||||||
|
label: 'Ngân hàng:',
|
||||||
|
value: bankName ?? 'BIDV',
|
||||||
|
),
|
||||||
|
|
||||||
const Divider(height: 24),
|
const Divider(height: 24),
|
||||||
|
|
||||||
@@ -289,7 +347,7 @@ class PaymentQrPage extends HookConsumerWidget {
|
|||||||
_buildInfoRow(
|
_buildInfoRow(
|
||||||
context: context,
|
context: context,
|
||||||
label: 'Số tài khoản:',
|
label: 'Số tài khoản:',
|
||||||
value: '19036810704016',
|
value: accountNo ?? '19036810704016',
|
||||||
),
|
),
|
||||||
|
|
||||||
const Divider(height: 24),
|
const Divider(height: 24),
|
||||||
@@ -298,7 +356,7 @@ class PaymentQrPage extends HookConsumerWidget {
|
|||||||
_buildInfoRow(
|
_buildInfoRow(
|
||||||
context: context,
|
context: context,
|
||||||
label: 'Chủ tài khoản:',
|
label: 'Chủ tài khoản:',
|
||||||
value: 'CÔNG TY EUROTILE',
|
value: accountName ?? 'CÔNG TY EUROTILE',
|
||||||
),
|
),
|
||||||
|
|
||||||
const Divider(height: 24),
|
const Divider(height: 24),
|
||||||
@@ -307,7 +365,7 @@ class PaymentQrPage extends HookConsumerWidget {
|
|||||||
_buildInfoRow(
|
_buildInfoRow(
|
||||||
context: context,
|
context: context,
|
||||||
label: 'Nội dung:',
|
label: 'Nội dung:',
|
||||||
value: '$orderId La Nguyen Quynh',
|
value: orderId,
|
||||||
),
|
),
|
||||||
|
|
||||||
const SizedBox(height: AppSpacing.md),
|
const SizedBox(height: AppSpacing.md),
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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';
|
||||||
@@ -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,
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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';
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
@@ -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';
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
@@ -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';
|
||||||
@@ -6,7 +6,6 @@ library;
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:worker/core/database/models/enums.dart';
|
import 'package:worker/core/database/models/enums.dart';
|
||||||
import 'package:worker/core/theme/colors.dart';
|
import 'package:worker/core/theme/colors.dart';
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
|
|||||||
// Show feedback
|
// Show feedback
|
||||||
final isFavorite = ref.read(isFavoriteProvider(widget.productId));
|
final isFavorite = ref.read(isFavoriteProvider(widget.productId));
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).clearSnackBars();
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text(
|
content: Text(
|
||||||
@@ -117,6 +118,7 @@ class _ProductDetailPageState extends ConsumerState<ProductDetailPage> {
|
|||||||
title: const Text('Chia sẻ qua tin nhắn'),
|
title: const Text('Chia sẻ qua tin nhắn'),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
|
ScaffoldMessenger.of(context).clearSnackBars();
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(
|
const SnackBar(
|
||||||
content: Text('Đang phát triển tính năng chat'),
|
content: Text('Đang phát triển tính năng chat'),
|
||||||
|
|||||||
@@ -34,7 +34,6 @@ class ProductsPage extends ConsumerWidget {
|
|||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final l10n = AppLocalizations.of(context);
|
final l10n = AppLocalizations.of(context);
|
||||||
final productsAsync = ref.watch(productsProvider);
|
final productsAsync = ref.watch(productsProvider);
|
||||||
final cartItemCount = ref.watch(cartItemCountProvider);
|
|
||||||
|
|
||||||
// Preload filter options for better UX when opening filter drawer
|
// Preload filter options for better UX when opening filter drawer
|
||||||
ref.watch(productFilterOptionsProvider);
|
ref.watch(productFilterOptionsProvider);
|
||||||
@@ -53,8 +52,11 @@ class ProductsPage extends ConsumerWidget {
|
|||||||
foregroundColor: AppColors.grey900,
|
foregroundColor: AppColors.grey900,
|
||||||
centerTitle: false,
|
centerTitle: false,
|
||||||
actions: [
|
actions: [
|
||||||
// Cart Icon with Badge
|
// Cart Icon with Badge (extracted to Consumer to prevent full page rebuild)
|
||||||
IconButton(
|
Consumer(
|
||||||
|
builder: (context, ref, child) {
|
||||||
|
final cartItemCount = ref.watch(cartItemCountProvider);
|
||||||
|
return IconButton(
|
||||||
icon: Badge(
|
icon: Badge(
|
||||||
label: Text('$cartItemCount'),
|
label: Text('$cartItemCount'),
|
||||||
backgroundColor: AppColors.danger,
|
backgroundColor: AppColors.danger,
|
||||||
@@ -63,6 +65,8 @@ class ProductsPage extends ConsumerWidget {
|
|||||||
child: const FaIcon(FontAwesomeIcons.cartShopping, color: Colors.black, size: 20),
|
child: const FaIcon(FontAwesomeIcons.cartShopping, color: Colors.black, size: 20),
|
||||||
),
|
),
|
||||||
onPressed: () => context.push(RouteNames.cart),
|
onPressed: () => context.push(RouteNames.cart),
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
const SizedBox(width: AppSpacing.sm),
|
const SizedBox(width: AppSpacing.sm),
|
||||||
],
|
],
|
||||||
@@ -129,13 +133,29 @@ class ProductsPage extends ConsumerWidget {
|
|||||||
// Add to cart
|
// Add to cart
|
||||||
ref.read(cartProvider.notifier).addToCart(product);
|
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(
|
SnackBar(
|
||||||
content: Text('${product.name} đã thêm vào giỏ hàng'),
|
content: Text('${product.name} đã thêm vào giỏ hàng'),
|
||||||
duration: const Duration(seconds: 2),
|
duration: const Duration(days: 365), // Prevent auto-dismiss
|
||||||
action: SnackBarAction(label: 'Xem', onPressed: () => context.go(RouteNames.cart)),
|
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();
|
||||||
|
});
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
|||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||||
# In Windows, build-name is used as the major, minor, and patch parts
|
# In Windows, build-name is used as the major, minor, and patch parts
|
||||||
# of the product and file versions while build-number is used as the build suffix.
|
# of the product and file versions while build-number is used as the build suffix.
|
||||||
version: 1.0.0+7
|
version: 1.0.0+8
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.10.0
|
sdk: ^3.10.0
|
||||||
|
|||||||
Reference in New Issue
Block a user