From 06b08348223693bd45e47d615f72cf9270da9197 Mon Sep 17 00:00:00 2001 From: Phuoc Nguyen Date: Fri, 21 Nov 2025 17:31:49 +0700 Subject: [PATCH] update gen qr --- docs/order.sh | 30 ++++- lib/core/constants/api_constants.dart | 6 + lib/core/router/app_router.dart | 5 +- .../widgets/checkout_submit_button.dart | 13 +- .../datasources/order_remote_datasource.dart | 46 ++++++- .../repositories/order_repository_impl.dart | 9 ++ .../domain/repositories/order_repository.dart | 3 + .../presentation/pages/payment_qr_page.dart | 124 +++++++++++++----- 8 files changed, 192 insertions(+), 44 deletions(-) diff --git a/docs/order.sh b/docs/order.sh index 88ef509..8f34e83 100644 --- a/docs/order.sh +++ b/docs/order.sh @@ -87,4 +87,32 @@ curl --location 'https://land.dbiz.com//api/method/building_material.building_ma "price_entered": 10000 // Đơn giá } ] - }' \ No newline at end of file + }' + + #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" + } + } +} \ No newline at end of file diff --git a/lib/core/constants/api_constants.dart b/lib/core/constants/api_constants.dart index f42f1d0..a93f447 100644 --- a/lib/core/constants/api_constants.dart +++ b/lib/core/constants/api_constants.dart @@ -221,6 +221,12 @@ class ApiConstants { /// 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 /orders?status={status}&page={page}&limit={limit} static const String getOrders = '/orders'; diff --git a/lib/core/router/app_router.dart b/lib/core/router/app_router.dart index 57a32e8..1b62964 100644 --- a/lib/core/router/app_router.dart +++ b/lib/core/router/app_router.dart @@ -323,7 +323,10 @@ final routerProvider = Provider((ref) { final amount = double.tryParse(amountStr) ?? 0.0; return MaterialPage( key: state.pageKey, - child: PaymentQrPage(orderId: orderId, amount: amount), + child: PaymentQrPage( + orderId: orderId, + amount: amount, + ), ); }, ), diff --git a/lib/features/cart/presentation/widgets/checkout_submit_button.dart b/lib/features/cart/presentation/widgets/checkout_submit_button.dart index 11236de..c9165a1 100644 --- a/lib/features/cart/presentation/widgets/checkout_submit_button.dart +++ b/lib/features/cart/presentation/widgets/checkout_submit_button.dart @@ -126,11 +126,6 @@ class CheckoutSubmitButton extends HookConsumerWidget { notes: notes, ).future); - // Close loading dialog - if (context.mounted) { - Navigator.of(context).pop(); - } - // Extract order number from response final orderNumber = result['orderNumber'] as String? ?? result['orderId'] as String? ?? @@ -139,6 +134,7 @@ class CheckoutSubmitButton extends HookConsumerWidget { if (needsNegotiation) { // Navigate to order success page with negotiation flag if (context.mounted) { + Navigator.of(context).pop(); context.pushReplacementNamed( RouteNames.orderSuccess, queryParameters: { @@ -149,7 +145,12 @@ class CheckoutSubmitButton extends HookConsumerWidget { ); } } else { - // Navigate to payment QR page + // 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, diff --git a/lib/features/orders/data/datasources/order_remote_datasource.dart b/lib/features/orders/data/datasources/order_remote_datasource.dart index 811218f..0c0c421 100644 --- a/lib/features/orders/data/datasources/order_remote_datasource.dart +++ b/lib/features/orders/data/datasources/order_remote_datasource.dart @@ -139,19 +139,59 @@ class OrderRemoteDataSource { } // Extract order info from Frappe response + // Response format: { message: { success: true, message: "...", data: { name: "SAL-ORD-2025-00078", ... } } } final message = data['message'] as Map?; if (message == null) { throw Exception('No message field in createOrder response'); } + final orderData = message['data'] as Map?; + 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': message['name'] ?? '', - 'orderNumber': message['name'] ?? '', - 'fullResponse': message, + '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> generateQrCode(String orderId) async { + try { + final response = await _dioClient.post>( + '${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?; + if (message == null) { + throw Exception('No message field in generateQrCode response'); + } + + return message; + } catch (e) { + throw Exception('Failed to generate QR code: $e'); + } + } } diff --git a/lib/features/orders/data/repositories/order_repository_impl.dart b/lib/features/orders/data/repositories/order_repository_impl.dart index 4f9112a..6bd61dc 100644 --- a/lib/features/orders/data/repositories/order_repository_impl.dart +++ b/lib/features/orders/data/repositories/order_repository_impl.dart @@ -56,4 +56,13 @@ class OrderRepositoryImpl implements OrderRepository { throw Exception('Failed to create order: $e'); } } + + @override + Future> generateQrCode(String orderId) async { + try { + return await _remoteDataSource.generateQrCode(orderId); + } catch (e) { + throw Exception('Failed to generate QR code: $e'); + } + } } diff --git a/lib/features/orders/domain/repositories/order_repository.dart b/lib/features/orders/domain/repositories/order_repository.dart index 03b6b53..a5ca754 100644 --- a/lib/features/orders/domain/repositories/order_repository.dart +++ b/lib/features/orders/domain/repositories/order_repository.dart @@ -23,4 +23,7 @@ abstract class OrderRepository { bool needsNegotiation = false, String? notes, }); + + /// Generate QR code for payment + Future> generateQrCode(String orderId); } diff --git a/lib/features/orders/presentation/pages/payment_qr_page.dart b/lib/features/orders/presentation/pages/payment_qr_page.dart index f3727db..2b73518 100644 --- a/lib/features/orders/presentation/pages/payment_qr_page.dart +++ b/lib/features/orders/presentation/pages/payment_qr_page.dart @@ -17,9 +17,11 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:qr_flutter/qr_flutter.dart'; import 'package:worker/core/constants/ui_constants.dart'; import 'package:worker/core/theme/colors.dart'; +import 'package:worker/features/orders/presentation/providers/order_repository_provider.dart'; /// Payment QR Page /// @@ -28,14 +30,43 @@ class PaymentQrPage extends HookConsumerWidget { final String orderId; final double amount; - const PaymentQrPage({super.key, required this.orderId, required this.amount}); + const PaymentQrPage({ + super.key, + required this.orderId, + required this.amount, + }); @override Widget build(BuildContext context, WidgetRef ref) { + // QR code data state + final qrCodeData = useState?>(null); + final isLoadingQr = useState(true); + final qrError = useState(null); + // Countdown timer (15 minutes = 900 seconds) final remainingSeconds = useState(900); final timer = useRef(null); + // Fetch QR code data + useEffect(() { + Future 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 useEffect(() { timer.value = Timer.periodic(const Duration(seconds: 1), (t) { @@ -94,12 +125,24 @@ class PaymentQrPage extends HookConsumerWidget { const SizedBox(height: AppSpacing.md), // QR Code Card - _buildQrCodeCard(amount, orderId), + _buildQrCodeCard( + amount, + orderId, + qrCodeData.value?['qr_code'] as String?, + isLoadingQr.value, + ), const SizedBox(height: AppSpacing.md), // 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), @@ -180,14 +223,12 @@ class PaymentQrPage extends HookConsumerWidget { } /// Build QR code card - Widget _buildQrCodeCard(double amount, String orderId) { - // Generate QR code data URL - final qrData = Uri.encodeComponent( - 'https://eurotile.com/payment/$orderId?amount=$amount', - ); - final qrUrl = - 'https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=$qrData'; - + Widget _buildQrCodeCard( + double amount, + String orderId, + String? qrCodeData, + bool isLoading, + ) { return Container( margin: const EdgeInsets.symmetric(horizontal: AppSpacing.md), padding: const EdgeInsets.all(AppSpacing.md), @@ -222,23 +263,29 @@ class PaymentQrPage extends HookConsumerWidget { borderRadius: BorderRadius.circular(AppRadius.card), border: Border.all(color: const Color(0xFFE2E8F0)), ), - child: Image.network( - qrUrl, - fit: BoxFit.contain, - errorBuilder: (context, error, stackTrace) { - return const Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - FaIcon(FontAwesomeIcons.qrcode, size: 80, color: AppColors.grey500), - SizedBox(height: 8), - Text( - 'Không thể tải mã QR', - style: TextStyle(fontSize: 12, color: AppColors.grey500), - ), - ], - ); - }, - ), + child: isLoading + ? const Center( + child: CircularProgressIndicator(), + ) + : qrCodeData != null && qrCodeData.isNotEmpty + ? QrImageView( + data: qrCodeData, + version: QrVersions.auto, + size: 200.0, + backgroundColor: Colors.white, + errorCorrectionLevel: QrErrorCorrectLevel.M, + ) + : const Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + FaIcon(FontAwesomeIcons.qrcode, size: 80, color: AppColors.grey500), + SizedBox(height: 8), + Text( + 'Không thể tải mã QR', + style: TextStyle(fontSize: 12, color: AppColors.grey500), + ), + ], + ), ), const SizedBox(height: AppSpacing.md), const Text( @@ -252,7 +299,14 @@ class PaymentQrPage extends HookConsumerWidget { } /// 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( margin: const EdgeInsets.symmetric(horizontal: AppSpacing.md), padding: const EdgeInsets.all(AppSpacing.md), @@ -281,7 +335,11 @@ class PaymentQrPage extends HookConsumerWidget { const SizedBox(height: AppSpacing.md), // 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), @@ -289,7 +347,7 @@ class PaymentQrPage extends HookConsumerWidget { _buildInfoRow( context: context, label: 'Số tài khoản:', - value: '19036810704016', + value: accountNo ?? '19036810704016', ), const Divider(height: 24), @@ -298,7 +356,7 @@ class PaymentQrPage extends HookConsumerWidget { _buildInfoRow( context: context, label: 'Chủ tài khoản:', - value: 'CÔNG TY EUROTILE', + value: accountName ?? 'CÔNG TY EUROTILE', ), const Divider(height: 24), @@ -307,7 +365,7 @@ class PaymentQrPage extends HookConsumerWidget { _buildInfoRow( context: context, label: 'Nội dung:', - value: '$orderId La Nguyen Quynh', + value: orderId, ), const SizedBox(height: AppSpacing.md),