create order -> upload bill
This commit is contained in:
@@ -11,15 +11,18 @@
|
||||
library;
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
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:image_picker/image_picker.dart';
|
||||
import 'package:qr_flutter/qr_flutter.dart';
|
||||
|
||||
import 'package:worker/core/constants/ui_constants.dart';
|
||||
import 'package:worker/core/router/app_router.dart';
|
||||
import 'package:worker/core/theme/colors.dart';
|
||||
import 'package:worker/features/orders/presentation/providers/order_repository_provider.dart';
|
||||
|
||||
@@ -47,6 +50,10 @@ class PaymentQrPage extends HookConsumerWidget {
|
||||
final remainingSeconds = useState<int>(900);
|
||||
final timer = useRef<Timer?>(null);
|
||||
|
||||
// Upload state
|
||||
final isUploadingBill = useState<bool>(false);
|
||||
final selectedImagePath = useState<String?>(null);
|
||||
|
||||
// Fetch QR code data
|
||||
useEffect(() {
|
||||
Future<void> fetchQrCode() async {
|
||||
@@ -146,8 +153,27 @@ class PaymentQrPage extends HookConsumerWidget {
|
||||
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
|
||||
// Image Preview Section
|
||||
_buildImagePreviewSection(
|
||||
context,
|
||||
selectedImagePath.value,
|
||||
() async {
|
||||
await _selectImage(context, selectedImagePath);
|
||||
},
|
||||
),
|
||||
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
|
||||
// Action Buttons
|
||||
_buildActionButtons(context),
|
||||
_buildActionButtons(
|
||||
context,
|
||||
ref,
|
||||
isUploadingBill.value,
|
||||
selectedImagePath.value != null,
|
||||
() async {
|
||||
await _uploadBill(context, ref, selectedImagePath, isUploadingBill);
|
||||
},
|
||||
),
|
||||
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
|
||||
@@ -452,44 +478,174 @@ class PaymentQrPage extends HookConsumerWidget {
|
||||
);
|
||||
}
|
||||
|
||||
/// Build action buttons
|
||||
Widget _buildActionButtons(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
|
||||
child: Row(
|
||||
/// Build image preview section
|
||||
Widget _buildImagePreviewSection(
|
||||
BuildContext context,
|
||||
String? imagePath,
|
||||
VoidCallback onSelectImage,
|
||||
) {
|
||||
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: [
|
||||
// Confirmed Payment Button
|
||||
Expanded(
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: () => _confirmPayment(context),
|
||||
icon: const FaIcon(FontAwesomeIcons.check, size: 18),
|
||||
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 Text(
|
||||
'Ảnh hóa đơn',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF212121),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: AppSpacing.sm),
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
|
||||
// Upload Proof Button
|
||||
Expanded(
|
||||
// Image preview or placeholder
|
||||
InkWell(
|
||||
onTap: onSelectImage,
|
||||
borderRadius: BorderRadius.circular(AppRadius.card),
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
height: 200,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF4F6F8),
|
||||
borderRadius: BorderRadius.circular(AppRadius.card),
|
||||
border: Border.all(
|
||||
color: const Color(0xFFE2E8F0),
|
||||
width: 2,
|
||||
style: BorderStyle.solid,
|
||||
),
|
||||
),
|
||||
child: imagePath != null
|
||||
? ClipRRect(
|
||||
borderRadius: BorderRadius.circular(AppRadius.card - 2),
|
||||
child: Stack(
|
||||
children: [
|
||||
Image.file(
|
||||
File(imagePath),
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
Positioned(
|
||||
top: 8,
|
||||
right: 8,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withValues(alpha: 0.6),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 6,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: const [
|
||||
FaIcon(
|
||||
FontAwesomeIcons.pen,
|
||||
color: Colors.white,
|
||||
size: 12,
|
||||
),
|
||||
SizedBox(width: 6),
|
||||
Text(
|
||||
'Đổi ảnh',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
width: 60,
|
||||
height: 60,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.primaryBlue.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
),
|
||||
child: const Icon(
|
||||
FontAwesomeIcons.image,
|
||||
color: AppColors.primaryBlue,
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const Text(
|
||||
'Chạm để chọn ảnh hóa đơn',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: AppColors.grey500,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
const Text(
|
||||
'Hỗ trợ: JPG, PNG',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppColors.grey500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Build action buttons
|
||||
Widget _buildActionButtons(
|
||||
BuildContext context,
|
||||
WidgetRef ref,
|
||||
bool isUploading,
|
||||
bool hasImage,
|
||||
VoidCallback onUpload,
|
||||
) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
|
||||
child: Column(
|
||||
children: [
|
||||
// Upload Bill Button
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () => _uploadProof(context),
|
||||
icon: const FaIcon(FontAwesomeIcons.camera, size: 18),
|
||||
label: const Text(
|
||||
'Upload bill',
|
||||
style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600),
|
||||
onPressed: (isUploading || !hasImage) ? null : onUpload,
|
||||
icon: isUploading
|
||||
? const SizedBox(
|
||||
width: 18,
|
||||
height: 18,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
|
||||
),
|
||||
)
|
||||
: const FaIcon(FontAwesomeIcons.camera, size: 18),
|
||||
label: Text(
|
||||
isUploading ? 'Đang upload...' : 'Upload bill chuyển khoản',
|
||||
style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w600),
|
||||
),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.primaryBlue,
|
||||
@@ -499,6 +655,33 @@ class PaymentQrPage extends HookConsumerWidget {
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(AppRadius.button),
|
||||
),
|
||||
disabledBackgroundColor: AppColors.grey100,
|
||||
disabledForegroundColor: AppColors.grey500,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.sm),
|
||||
|
||||
// Back to Home Button
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: isUploading ? null : () => context.goNamed(RouteNames.home),
|
||||
icon: const FaIcon(FontAwesomeIcons.house, size: 18),
|
||||
label: const Text(
|
||||
'Quay về trang chủ',
|
||||
style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600),
|
||||
),
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: AppColors.grey900,
|
||||
side: const BorderSide(
|
||||
color: AppColors.grey100,
|
||||
width: 1.5,
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(AppRadius.button),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -598,57 +781,209 @@ class PaymentQrPage extends HookConsumerWidget {
|
||||
);
|
||||
}
|
||||
|
||||
/// Confirm payment
|
||||
void _confirmPayment(BuildContext context) {
|
||||
showDialog<void>(
|
||||
/// Select image for bill
|
||||
Future<void> _selectImage(
|
||||
BuildContext context,
|
||||
ValueNotifier<String?> selectedImagePath,
|
||||
) async {
|
||||
// Show bottom sheet to select camera or gallery
|
||||
final ImageSource? source = await showModalBottomSheet<ImageSource>(
|
||||
context: context,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(AppRadius.card)),
|
||||
),
|
||||
builder: (context) => Container(
|
||||
padding: const EdgeInsets.all(AppSpacing.md),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text(
|
||||
'Chọn ảnh hóa đơn',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF212121),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
ListTile(
|
||||
leading: const FaIcon(
|
||||
FontAwesomeIcons.camera,
|
||||
color: AppColors.primaryBlue,
|
||||
),
|
||||
title: const Text('Chụp ảnh'),
|
||||
onTap: () => Navigator.of(context).pop(ImageSource.camera),
|
||||
),
|
||||
ListTile(
|
||||
leading: const FaIcon(
|
||||
FontAwesomeIcons.image,
|
||||
color: AppColors.primaryBlue,
|
||||
),
|
||||
title: const Text('Chọn từ thư viện'),
|
||||
onTap: () => Navigator.of(context).pop(ImageSource.gallery),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.sm),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('Hủy'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
if (source == null || !context.mounted) return;
|
||||
|
||||
try {
|
||||
// Pick image
|
||||
final picker = ImagePicker();
|
||||
final pickedFile = await picker.pickImage(
|
||||
source: source,
|
||||
maxWidth: 1920,
|
||||
maxHeight: 1080,
|
||||
imageQuality: 85,
|
||||
);
|
||||
|
||||
if (pickedFile == null || !context.mounted) return;
|
||||
|
||||
selectedImagePath.value = pickedFile.path;
|
||||
|
||||
// Show success feedback
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Row(
|
||||
children: [
|
||||
FaIcon(FontAwesomeIcons.circleCheck, color: Colors.white, size: 20),
|
||||
SizedBox(width: 12),
|
||||
Text('Đã chọn ảnh. Nhấn "Upload bill chuyển khoản" để gửi.'),
|
||||
],
|
||||
),
|
||||
backgroundColor: AppColors.success,
|
||||
duration: Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
if (!context.mounted) return;
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Lỗi khi chọn ảnh: ${e.toString()}'),
|
||||
backgroundColor: AppColors.danger,
|
||||
duration: const Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Upload bill to server
|
||||
Future<void> _uploadBill(
|
||||
BuildContext context,
|
||||
WidgetRef ref,
|
||||
ValueNotifier<String?> selectedImagePath,
|
||||
ValueNotifier<bool> isUploadingBill,
|
||||
) async {
|
||||
if (selectedImagePath.value == null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Vui lòng chọn ảnh hóa đơn trước'),
|
||||
backgroundColor: AppColors.danger,
|
||||
duration: Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Show confirmation dialog
|
||||
final bool? confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Xác nhận thanh toán'),
|
||||
title: const Text('Xác nhận upload'),
|
||||
content: const Text(
|
||||
'Bạn đã hoàn tất thanh toán cho đơn hàng này?',
|
||||
'Bạn có muốn upload hóa đơn này không?',
|
||||
style: TextStyle(fontSize: 14),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('Chưa'),
|
||||
onPressed: () => Navigator.of(context).pop(false),
|
||||
child: const Text('Hủy'),
|
||||
),
|
||||
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();
|
||||
}
|
||||
});
|
||||
},
|
||||
onPressed: () => Navigator.of(context).pop(true),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.primaryBlue,
|
||||
),
|
||||
child: const Text('Đã thanh toán'),
|
||||
child: const Text('Upload'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 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),
|
||||
),
|
||||
);
|
||||
if (confirmed != true || !context.mounted) return;
|
||||
|
||||
try {
|
||||
// Start upload
|
||||
isUploadingBill.value = true;
|
||||
|
||||
final repository = await ref.read(orderRepositoryProvider.future);
|
||||
final result = await repository.uploadBill(
|
||||
filePath: selectedImagePath.value!,
|
||||
orderId: orderId,
|
||||
);
|
||||
|
||||
if (!context.mounted) return;
|
||||
|
||||
isUploadingBill.value = false;
|
||||
|
||||
// Show success message and navigate
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Row(
|
||||
children: [
|
||||
FaIcon(FontAwesomeIcons.circleCheck, color: Colors.white, size: 20),
|
||||
SizedBox(width: 12),
|
||||
Text('Upload hóa đơn thành công!'),
|
||||
],
|
||||
),
|
||||
backgroundColor: AppColors.success,
|
||||
duration: Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
|
||||
// Navigate to order success page after successful upload
|
||||
Future.delayed(const Duration(milliseconds: 500), () {
|
||||
if (context.mounted) {
|
||||
context.pushReplacementNamed(
|
||||
RouteNames.orderSuccess,
|
||||
queryParameters: {
|
||||
'orderNumber': orderId,
|
||||
'total': amount.toString(),
|
||||
'isNegotiation': 'false',
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
if (!context.mounted) return;
|
||||
|
||||
isUploadingBill.value = false;
|
||||
|
||||
// Show error message
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Row(
|
||||
children: [
|
||||
const FaIcon(FontAwesomeIcons.circleXmark, color: Colors.white, size: 20),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text('Lỗi upload: ${e.toString()}'),
|
||||
),
|
||||
],
|
||||
),
|
||||
backgroundColor: AppColors.danger,
|
||||
duration: const Duration(seconds: 3),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Format currency
|
||||
|
||||
Reference in New Issue
Block a user