From 988216b151056c8ecf85add23eed28fdf4f85d1d Mon Sep 17 00:00:00 2001 From: Phuoc Nguyen Date: Mon, 3 Nov 2025 16:16:15 +0700 Subject: [PATCH] payment page --- lib/core/router/app_router.dart | 17 + .../widgets/checkout_submit_button.dart | 36 +- .../presentation/pages/payment_qr_page.dart | 605 ++++++++++++++++++ 3 files changed, 649 insertions(+), 9 deletions(-) create mode 100644 lib/features/orders/presentation/pages/payment_qr_page.dart diff --git a/lib/core/router/app_router.dart b/lib/core/router/app_router.dart index 88b0e03..2fe0276 100644 --- a/lib/core/router/app_router.dart +++ b/lib/core/router/app_router.dart @@ -16,6 +16,7 @@ import 'package:worker/features/main/presentation/pages/main_scaffold.dart'; import 'package:worker/features/orders/presentation/pages/order_detail_page.dart'; import 'package:worker/features/orders/presentation/pages/orders_page.dart'; import 'package:worker/features/orders/presentation/pages/payment_detail_page.dart'; +import 'package:worker/features/orders/presentation/pages/payment_qr_page.dart'; import 'package:worker/features/orders/presentation/pages/payments_page.dart'; import 'package:worker/features/products/presentation/pages/product_detail_page.dart'; import 'package:worker/features/products/presentation/pages/products_page.dart'; @@ -176,6 +177,21 @@ class AppRouter { }, ), + // Payment QR Route + GoRoute( + path: RouteNames.paymentQr, + name: RouteNames.paymentQr, + pageBuilder: (context, state) { + final orderId = state.uri.queryParameters['orderId'] ?? ''; + final amountStr = state.uri.queryParameters['amount'] ?? '0'; + final amount = double.tryParse(amountStr) ?? 0.0; + return MaterialPage( + key: state.pageKey, + child: PaymentQrPage(orderId: orderId, amount: amount), + ); + }, + ), + // Quotes Route GoRoute( path: RouteNames.quotes, @@ -328,6 +344,7 @@ class RouteNames { static const String orderDetail = '/orders/:id'; static const String payments = '/payments'; static const String paymentDetail = '/payments/:id'; + static const String paymentQr = '/payment-qr'; // Projects & Quotes Routes static const String projects = '/projects'; diff --git a/lib/features/cart/presentation/widgets/checkout_submit_button.dart b/lib/features/cart/presentation/widgets/checkout_submit_button.dart index dac9fac..120c928 100644 --- a/lib/features/cart/presentation/widgets/checkout_submit_button.dart +++ b/lib/features/cart/presentation/widgets/checkout_submit_button.dart @@ -5,7 +5,9 @@ library; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; + import 'package:worker/core/constants/ui_constants.dart'; +import 'package:worker/core/router/app_router.dart'; import 'package:worker/core/theme/colors.dart'; /// Checkout Submit Button @@ -103,22 +105,38 @@ class CheckoutSubmitButton extends StatelessWidget { duration: Duration(seconds: 2), ), ); + + // Navigate back after a short delay + Future.delayed(const Duration(milliseconds: 500), () { + if (context.mounted) { + context.pop(); + } + }); } else { + // Generate order ID (mock - replace with actual from backend) + final orderId = 'DH${DateTime.now().millisecondsSinceEpoch.toString().substring(7)}'; + // Show order success message ScaffoldMessenger.of(context).showSnackBar( const SnackBar( - content: Text('Đặt hàng thành công!'), + content: Text('Đặt hàng thành công! Chuyển đến thanh toán...'), backgroundColor: AppColors.success, - duration: Duration(seconds: 2), + duration: Duration(seconds: 1), ), ); - } - // Navigate back after a short delay - Future.delayed(const Duration(milliseconds: 500), () { - if (context.mounted) { - context.pop(); - } - }); + // 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(), + }, + ); + } + }); + } } } diff --git a/lib/features/orders/presentation/pages/payment_qr_page.dart b/lib/features/orders/presentation/pages/payment_qr_page.dart new file mode 100644 index 0000000..41ddd39 --- /dev/null +++ b/lib/features/orders/presentation/pages/payment_qr_page.dart @@ -0,0 +1,605 @@ +/// Payment QR Page +/// +/// QR code payment screen with bank transfer information. +/// Features: +/// - Payment amount display with minimum payment warning +/// - QR code for quick payment +/// - Bank transfer information with copy buttons +/// - Payment confirmation and proof upload buttons +/// - Countdown timer for payment +/// - Payment instructions modal +library; + +import 'dart:async'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_hooks/flutter_hooks.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/theme/colors.dart'; + +/// Payment QR Page +/// +/// Displays QR code and bank transfer information for payment. +class PaymentQrPage extends HookConsumerWidget { + final String orderId; + final double amount; + + const PaymentQrPage({ + super.key, + required this.orderId, + required this.amount, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + // Countdown timer (15 minutes = 900 seconds) + final remainingSeconds = useState(900); + final timer = useRef(null); + + // Start countdown timer + useEffect(() { + timer.value = Timer.periodic(const Duration(seconds: 1), (t) { + if (remainingSeconds.value > 0) { + remainingSeconds.value--; + } else { + t.cancel(); + } + }); + + return () { + timer.value?.cancel(); + }; + }, []); + + // Format timer display + final minutes = remainingSeconds.value ~/ 60; + final seconds = remainingSeconds.value % 60; + final timerDisplay = + '${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}'; + + return Scaffold( + backgroundColor: const Color(0xFFF4F6F8), + appBar: AppBar( + backgroundColor: Colors.white, + elevation: 0, + leading: IconButton( + icon: const Icon(Icons.arrow_back, color: Colors.black), + onPressed: () => context.pop(), + ), + title: const Text( + 'Thanh toán', + style: TextStyle( + color: Colors.black, + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + centerTitle: false, + actions: [ + IconButton( + icon: const Icon(Icons.info_outline, color: Colors.black), + onPressed: () => _showInfoDialog(context), + ), + const SizedBox(width: AppSpacing.sm), + ], + ), + body: SingleChildScrollView( + child: Column( + children: [ + const SizedBox(height: AppSpacing.md), + + // Payment Amount Card + _buildAmountCard(amount), + + const SizedBox(height: AppSpacing.md), + + // QR Code Card + _buildQrCodeCard(amount, orderId), + + const SizedBox(height: AppSpacing.md), + + // Bank Transfer Info Card + _buildBankInfoCard(context, orderId), + + const SizedBox(height: AppSpacing.md), + + // Action Buttons + _buildActionButtons(context), + + const SizedBox(height: AppSpacing.md), + + // Timer + _buildTimer(timerDisplay, remainingSeconds.value), + + const SizedBox(height: AppSpacing.lg), + ], + ), + ), + ); + } + + /// Build payment amount card + Widget _buildAmountCard(double amount) { + 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: Column( + children: [ + Text( + _formatCurrency(amount), + style: const TextStyle( + fontSize: 28, + fontWeight: FontWeight.bold, + color: AppColors.primaryBlue, + ), + ), + const SizedBox(height: 8), + const Text( + 'Số tiền cần thanh toán', + style: TextStyle(fontSize: 14, color: AppColors.grey500), + ), + const SizedBox(height: 12), + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: const Color(0xFFFFF8E1), + borderRadius: BorderRadius.circular(AppRadius.card), + border: Border.all(color: const Color(0xFFFFD54F)), + ), + child: Row( + children: [ + const Icon(Icons.info, color: AppColors.warning, size: 20), + const SizedBox(width: 8), + Expanded( + child: Text( + 'Thanh toán không dưới 20%', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: Colors.orange[900], + ), + ), + ), + ], + ), + ), + ], + ), + ); + } + + /// 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'; + + 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: Column( + children: [ + const Text( + 'Quét mã QR để thanh toán', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Color(0xFF212121), + ), + ), + const SizedBox(height: AppSpacing.md), + Container( + width: 220, + height: 220, + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: Colors.white, + 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: [ + Icon(Icons.qr_code, 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( + 'Quét mã QR bằng ứng dụng ngân hàng để thanh toán nhanh chóng', + textAlign: TextAlign.center, + style: TextStyle(fontSize: 14, color: AppColors.grey500), + ), + ], + ), + ); + } + + /// Build bank transfer info card + Widget _buildBankInfoCard(BuildContext context, String orderId) { + 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: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Thông tin chuyển khoản', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Color(0xFF212121), + ), + ), + const SizedBox(height: AppSpacing.md), + + // Bank Name + _buildInfoRow( + context: context, + label: 'Ngân hàng:', + value: 'BIDV', + ), + + const Divider(height: 24), + + // Account Number + _buildInfoRow( + context: context, + label: 'Số tài khoản:', + value: '19036810704016', + ), + + const Divider(height: 24), + + // Account Holder + _buildInfoRow( + context: context, + label: 'Chủ tài khoản:', + value: 'CÔNG TY EUROTILE', + ), + + const Divider(height: 24), + + // Transfer Content + _buildInfoRow( + context: context, + label: 'Nội dung:', + value: '$orderId La Nguyen Quynh', + ), + + const SizedBox(height: AppSpacing.md), + + // Note + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: const Color(0xFFE3F2FD), + borderRadius: BorderRadius.circular(AppRadius.card), + border: Border.all(color: const Color(0xFF90CAF9)), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Icon(Icons.lightbulb_outline, + color: AppColors.primaryBlue, size: 20), + const SizedBox(width: 8), + Expanded( + child: RichText( + text: TextSpan( + style: const TextStyle( + fontSize: 13, + color: Color(0xFF1565C0), + height: 1.4, + ), + children: [ + const TextSpan( + text: 'Lưu ý: ', + style: TextStyle(fontWeight: FontWeight.bold), + ), + const TextSpan( + text: + 'Vui lòng ghi đúng nội dung chuyển khoản để đơn hàng được xử lý nhanh chóng.', + ), + ], + ), + ), + ), + ], + ), + ), + ], + ), + ); + } + + /// Build info row with copy button + Widget _buildInfoRow({ + required BuildContext context, + required String label, + required String value, + }) { + return Row( + children: [ + Expanded( + flex: 2, + child: Text( + label, + style: const TextStyle(fontSize: 14, color: AppColors.grey500), + ), + ), + Expanded( + flex: 3, + child: Text( + value, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: Color(0xFF212121), + ), + ), + ), + IconButton( + icon: const Icon(Icons.copy, size: 20, color: AppColors.primaryBlue), + onPressed: () => _copyToClipboard(context, value), + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + ), + ], + ); + } + + /// Build action buttons + Widget _buildActionButtons(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: AppSpacing.md), + child: Row( + children: [ + // Confirmed Payment Button + Expanded( + child: OutlinedButton.icon( + onPressed: () => _confirmPayment(context), + icon: const Icon(Icons.check, size: 20), + label: const Text( + 'Đã thanh toán', + style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600), + ), + style: OutlinedButton.styleFrom( + foregroundColor: AppColors.primaryBlue, + side: const BorderSide(color: AppColors.primaryBlue, width: 1.5), + padding: const EdgeInsets.symmetric(vertical: 14), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(AppRadius.button), + ), + ), + ), + ), + const SizedBox(width: AppSpacing.sm), + + // Upload Proof Button + Expanded( + child: ElevatedButton.icon( + onPressed: () => _uploadProof(context), + icon: const Icon(Icons.camera_alt, size: 20), + label: const Text( + 'Upload bill', + style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600), + ), + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.primaryBlue, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 14), + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(AppRadius.button), + ), + ), + ), + ), + ], + ), + ); + } + + /// Build countdown timer + Widget _buildTimer(String timerDisplay, int remainingSeconds) { + return Container( + margin: const EdgeInsets.symmetric(horizontal: AppSpacing.md), + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(AppRadius.card), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.schedule, size: 18, color: AppColors.grey500), + const SizedBox(width: 8), + const Text( + 'Thời gian thanh toán: ', + style: TextStyle(fontSize: 14, color: AppColors.grey500), + ), + Text( + timerDisplay, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: remainingSeconds < 300 + ? AppColors.danger + : AppColors.grey900, + ), + ), + ], + ), + ); + } + + /// Copy text to clipboard + void _copyToClipboard(BuildContext context, String text) { + Clipboard.setData(ClipboardData(text: text)); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Đã sao chép: $text'), + duration: const Duration(seconds: 1), + behavior: SnackBarBehavior.floating, + ), + ); + } + + /// Show payment info dialog + void _showInfoDialog(BuildContext context) { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Hướng dẫn thanh toán'), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Đây là nội dung hướng dẫn sử dụng cho tính năng Thanh toán:', + style: TextStyle(fontSize: 14), + ), + const SizedBox(height: 12), + _buildInfoItem('Quét mã QR bằng app ngân hàng hoặc ví điện tử'), + _buildInfoItem('Chuyển khoản theo thông tin được cung cấp'), + _buildInfoItem('Ghi đúng nội dung chuyển khoản'), + _buildInfoItem('Upload hóa đơn sau khi chuyển khoản'), + _buildInfoItem('Thanh toán tối thiểu 20% giá trị đơn hàng'), + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('Đóng'), + ), + ], + ), + ); + } + + /// Build info item for dialog + Widget _buildInfoItem(String text) { + return Padding( + padding: const EdgeInsets.only(bottom: 8), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('• ', style: TextStyle(fontSize: 14)), + Expanded( + child: Text(text, style: const TextStyle(fontSize: 14)), + ), + ], + ), + ); + } + + /// Confirm payment + void _confirmPayment(BuildContext context) { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Xác nhận thanh toán'), + content: const Text( + 'Bạn đã hoàn tất thanh toán cho đơn hàng này?', + style: TextStyle(fontSize: 14), + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('Chưa'), + ), + ElevatedButton( + onPressed: () { + Navigator.of(context).pop(); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Đã xác nhận thanh toán!'), + backgroundColor: AppColors.success, + duration: Duration(seconds: 2), + ), + ); + // Navigate back after delay + Future.delayed(const Duration(milliseconds: 500), () { + if (context.mounted) { + context.pop(); + } + }); + }, + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.primaryBlue, + ), + child: const Text('Đã thanh toán'), + ), + ], + ), + ); + } + + /// Upload payment proof + void _uploadProof(BuildContext context) { + // TODO: Implement image picker and upload + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Tính năng upload bill đang được phát triển'), + duration: Duration(seconds: 2), + ), + ); + } + + /// Format currency + String _formatCurrency(double amount) { + return '${amount.toStringAsFixed(0).replaceAllMapped( + RegExp(r'(\d)(?=(\d{3})+(?!\d))'), + (Match m) => '${m[1]}.', + )}₫'; + } +}