update gen qr
This commit is contained in:
@@ -87,4 +87,32 @@ curl --location 'https://land.dbiz.com//api/method/building_material.building_ma
|
|||||||
"price_entered": 10000 // Đơn giá
|
"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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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';
|
||||||
|
|||||||
@@ -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,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,23 +263,29 @@ 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
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
? QrImageView(
|
||||||
children: [
|
data: qrCodeData,
|
||||||
FaIcon(FontAwesomeIcons.qrcode, size: 80, color: AppColors.grey500),
|
version: QrVersions.auto,
|
||||||
SizedBox(height: 8),
|
size: 200.0,
|
||||||
Text(
|
backgroundColor: Colors.white,
|
||||||
'Không thể tải mã QR',
|
errorCorrectionLevel: QrErrorCorrectLevel.M,
|
||||||
style: TextStyle(fontSize: 12, color: AppColors.grey500),
|
)
|
||||||
),
|
: 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 SizedBox(height: AppSpacing.md),
|
||||||
const Text(
|
const Text(
|
||||||
@@ -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),
|
||||||
|
|||||||
Reference in New Issue
Block a user