update gen qr

This commit is contained in:
Phuoc Nguyen
2025-11-21 17:31:49 +07:00
parent 4913a4e04b
commit 06b0834822
8 changed files with 192 additions and 44 deletions

View File

@@ -88,3 +88,31 @@ curl --location 'https://land.dbiz.com//api/method/building_material.building_ma
} }
] ]
}' }'
#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"
}
}
}

View File

@@ -221,6 +221,12 @@ class ApiConstants {
/// Body: { "transaction_date": "...", "delivery_date": "...", "items": [...], ... } /// Body: { "transaction_date": "...", "delivery_date": "...", "items": [...], ... }
static const String createOrder = '/building_material.building_material.api.sales_order.save'; 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}
static const String getOrders = '/orders'; static const String getOrders = '/orders';

View File

@@ -323,7 +323,10 @@ 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,
),
); );
}, },
), ),

View File

@@ -126,11 +126,6 @@ class CheckoutSubmitButton extends HookConsumerWidget {
notes: notes, notes: notes,
).future); ).future);
// Close loading dialog
if (context.mounted) {
Navigator.of(context).pop();
}
// Extract order number from response // Extract order number from response
final orderNumber = result['orderNumber'] as String? ?? final orderNumber = result['orderNumber'] as String? ??
result['orderId'] as String? ?? result['orderId'] as String? ??
@@ -139,6 +134,7 @@ class CheckoutSubmitButton extends HookConsumerWidget {
if (needsNegotiation) { if (needsNegotiation) {
// Navigate to order success page with negotiation flag // Navigate to order success page with negotiation flag
if (context.mounted) { if (context.mounted) {
Navigator.of(context).pop();
context.pushReplacementNamed( context.pushReplacementNamed(
RouteNames.orderSuccess, RouteNames.orderSuccess,
queryParameters: { queryParameters: {
@@ -149,7 +145,12 @@ class CheckoutSubmitButton extends HookConsumerWidget {
); );
} }
} else { } 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) { if (context.mounted) {
context.pushReplacementNamed( context.pushReplacementNamed(
RouteNames.paymentQr, RouteNames.paymentQr,

View File

@@ -139,19 +139,59 @@ class OrderRemoteDataSource {
} }
// Extract order info from Frappe response // 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>?; final message = data['message'] as Map<String, dynamic>?;
if (message == null) { if (message == null) {
throw Exception('No message field in createOrder response'); 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 standardized response
return { return {
'orderId': message['name'] ?? '', 'orderId': orderId,
'orderNumber': message['name'] ?? '', 'orderNumber': orderId,
'fullResponse': message, 'fullResponse': orderData,
}; };
} catch (e) { } catch (e) {
throw Exception('Failed to create order: $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');
}
}
} }

View File

@@ -56,4 +56,13 @@ class OrderRepositoryImpl implements OrderRepository {
throw Exception('Failed to create order: $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');
}
}
} }

View File

@@ -23,4 +23,7 @@ abstract class OrderRepository {
bool needsNegotiation = false, bool needsNegotiation = false,
String? notes, String? notes,
}); });
/// Generate QR code for payment
Future<Map<String, dynamic>> generateQrCode(String orderId);
} }

View File

@@ -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),