create order -> upload bill
This commit is contained in:
@@ -116,3 +116,24 @@ curl --location 'https://land.dbiz.com//api/method/building_material.building_ma
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#upload bill
|
||||||
|
curl --location 'https://land.dbiz.com//api/method/upload_file' \
|
||||||
|
--header 'Cookie: sid=a0cbe3ea6f9a7e9cf083bbe3139eada68d2357eac0167bcc66cda17d; sid=a0cbe3ea6f9a7e9cf083bbe3139eada68d2357eac0167bcc66cda17d' \
|
||||||
|
--header 'X-Frappe-Csrf-Token: 6ff3be4d1f887dbebf86ba4502b05d94b30c0b0569de49b74a7171a9' \
|
||||||
|
--form 'file=@"/C:/Users/tiennld/Downloads/logo_crm.png"' \
|
||||||
|
--form 'is_private="1"' \
|
||||||
|
--form 'folder="Home/Attachments"' \
|
||||||
|
--form 'doctype="Sales Order"' \
|
||||||
|
--form 'docname="SAL-ORD-2025-00058-1"' \
|
||||||
|
--form 'optimize="true"'
|
||||||
|
|
||||||
|
|
||||||
|
#order detail
|
||||||
|
curl --location 'https://land.dbiz.com//api/method/building_material.building_material.api.sales_order.get_detail' \
|
||||||
|
--header 'Cookie: sid=a0cbe3ea6f9a7e9cf083bbe3139eada68d2357eac0167bcc66cda17d; sid=a0cbe3ea6f9a7e9cf083bbe3139eada68d2357eac0167bcc66cda17d' \
|
||||||
|
--header 'X-Frappe-Csrf-Token: 6ff3be4d1f887dbebf86ba4502b05d94b30c0b0569de49b74a7171a9' \
|
||||||
|
--header 'Content-Type: application/json' \
|
||||||
|
--data '{
|
||||||
|
"name" : "SAL-ORD-2025-00058-1"
|
||||||
|
}'
|
||||||
@@ -12,9 +12,11 @@
|
|||||||
<div class="page-wrapper">
|
<div class="page-wrapper">
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<a href="checkout.html" class="back-button">
|
<!--<a href="checkout.html" class="back-button">
|
||||||
<i class="fas fa-arrow-left"></i>
|
<i class="fas fa-arrow-left"></i>
|
||||||
</a>
|
</a>-->
|
||||||
|
<div style="width: 32px;"></div>
|
||||||
|
|
||||||
<h1 class="header-title">Thanh toán</h1>
|
<h1 class="header-title">Thanh toán</h1>
|
||||||
<button class="back-button" onclick="openInfoModal()">
|
<button class="back-button" onclick="openInfoModal()">
|
||||||
<i class="fas fa-info-circle"></i>
|
<i class="fas fa-info-circle"></i>
|
||||||
|
|||||||
@@ -227,6 +227,12 @@ class ApiConstants {
|
|||||||
/// Returns: { "message": { "qr_code": "...", "amount": null, "transaction_id": "...", "bank_info": {...} } }
|
/// Returns: { "message": { "qr_code": "...", "amount": null, "transaction_id": "...", "bank_info": {...} } }
|
||||||
static const String generateQrCode = '/building_material.building_material.api.v1.qrcode.generate';
|
static const String generateQrCode = '/building_material.building_material.api.v1.qrcode.generate';
|
||||||
|
|
||||||
|
/// Upload file (bill/invoice/attachment) (requires sid and csrf_token)
|
||||||
|
/// POST /api/method/upload_file
|
||||||
|
/// Form-data: { "file": File, "is_private": "1", "folder": "Home/Attachments", "doctype": "Sales Order", "docname": "SAL-ORD-2025-00058-1", "optimize": "true" }
|
||||||
|
/// Returns: { "message": { "file_url": "...", "file_name": "...", ... } }
|
||||||
|
static const String uploadFile = '/upload_file';
|
||||||
|
|
||||||
/// 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';
|
||||||
|
|||||||
@@ -59,6 +59,8 @@ class CheckoutPage extends HookConsumerWidget {
|
|||||||
// Price negotiation
|
// Price negotiation
|
||||||
final needsNegotiation = useState<bool>(false);
|
final needsNegotiation = useState<bool>(false);
|
||||||
|
|
||||||
|
final needsContract = useState(false);
|
||||||
|
|
||||||
// Watch API provider for payment terms
|
// Watch API provider for payment terms
|
||||||
final paymentTermsListAsync = ref.watch(paymentTermsListProvider);
|
final paymentTermsListAsync = ref.watch(paymentTermsListProvider);
|
||||||
|
|
||||||
@@ -240,6 +242,40 @@ class CheckoutPage extends HookConsumerWidget {
|
|||||||
// Price Negotiation Section
|
// Price Negotiation Section
|
||||||
PriceNegotiationSection(needsNegotiation: needsNegotiation),
|
PriceNegotiationSection(needsNegotiation: needsNegotiation),
|
||||||
|
|
||||||
|
const SizedBox(height: AppSpacing.md),
|
||||||
|
|
||||||
|
|
||||||
|
Container(
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0xFFFFF8E1),
|
||||||
|
borderRadius: BorderRadius.circular(AppRadius.card),
|
||||||
|
border: Border.all(color: const Color(0xFFFFD54F)),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Checkbox(
|
||||||
|
value: needsContract.value,
|
||||||
|
onChanged: (value) {
|
||||||
|
needsContract.value = value ?? false;
|
||||||
|
},
|
||||||
|
activeColor: AppColors.warning,
|
||||||
|
),
|
||||||
|
const Expanded(
|
||||||
|
child: Text(
|
||||||
|
'Yêu cầu hợp đồng',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 15,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: Color(0xFF212121),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
|
||||||
const SizedBox(height: AppSpacing.md),
|
const SizedBox(height: AppSpacing.md),
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
/// Handles API calls for order-related data.
|
/// Handles API calls for order-related data.
|
||||||
library;
|
library;
|
||||||
|
|
||||||
|
import 'package:dio/dio.dart';
|
||||||
import 'package:worker/core/constants/api_constants.dart';
|
import 'package:worker/core/constants/api_constants.dart';
|
||||||
import 'package:worker/core/network/dio_client.dart';
|
import 'package:worker/core/network/dio_client.dart';
|
||||||
import 'package:worker/features/orders/data/models/order_status_model.dart';
|
import 'package:worker/features/orders/data/models/order_status_model.dart';
|
||||||
@@ -194,4 +195,56 @@ class OrderRemoteDataSource {
|
|||||||
throw Exception('Failed to generate QR code: $e');
|
throw Exception('Failed to generate QR code: $e');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Upload bill/invoice file
|
||||||
|
///
|
||||||
|
/// Calls: POST /api/method/upload_file
|
||||||
|
/// Form-data: {
|
||||||
|
/// "file": File,
|
||||||
|
/// "is_private": "1",
|
||||||
|
/// "folder": "Home/Attachments",
|
||||||
|
/// "doctype": "Sales Order",
|
||||||
|
/// "docname": "SAL-ORD-2025-00058-1",
|
||||||
|
/// "optimize": "true"
|
||||||
|
/// }
|
||||||
|
/// Returns: { "message": { "file_url": "...", "file_name": "...", ... } }
|
||||||
|
Future<Map<String, dynamic>> uploadBill({
|
||||||
|
required String filePath,
|
||||||
|
required String orderId,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
// Create multipart form data
|
||||||
|
final formData = FormData.fromMap({
|
||||||
|
'file': await MultipartFile.fromFile(
|
||||||
|
filePath,
|
||||||
|
filename: filePath.split('/').last,
|
||||||
|
),
|
||||||
|
'is_private': '1',
|
||||||
|
'folder': 'Home/Attachments',
|
||||||
|
'doctype': 'Sales Order',
|
||||||
|
'docname': orderId,
|
||||||
|
'optimize': 'true',
|
||||||
|
});
|
||||||
|
|
||||||
|
final response = await _dioClient.post<Map<String, dynamic>>(
|
||||||
|
'${ApiConstants.frappeApiMethod}${ApiConstants.uploadFile}',
|
||||||
|
data: formData,
|
||||||
|
);
|
||||||
|
|
||||||
|
final data = response.data;
|
||||||
|
if (data == null) {
|
||||||
|
throw Exception('No data received from uploadBill API');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract file info from Frappe response
|
||||||
|
final message = data['message'] as Map<String, dynamic>?;
|
||||||
|
if (message == null) {
|
||||||
|
throw Exception('No message field in uploadBill response');
|
||||||
|
}
|
||||||
|
|
||||||
|
return message;
|
||||||
|
} catch (e) {
|
||||||
|
throw Exception('Failed to upload bill: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,4 +65,19 @@ class OrderRepositoryImpl implements OrderRepository {
|
|||||||
throw Exception('Failed to generate QR code: $e');
|
throw Exception('Failed to generate QR code: $e');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Map<String, dynamic>> uploadBill({
|
||||||
|
required String filePath,
|
||||||
|
required String orderId,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
return await _remoteDataSource.uploadBill(
|
||||||
|
filePath: filePath,
|
||||||
|
orderId: orderId,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
throw Exception('Failed to upload bill: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,4 +26,10 @@ abstract class OrderRepository {
|
|||||||
|
|
||||||
/// Generate QR code for payment
|
/// Generate QR code for payment
|
||||||
Future<Map<String, dynamic>> generateQrCode(String orderId);
|
Future<Map<String, dynamic>> generateQrCode(String orderId);
|
||||||
|
|
||||||
|
/// Upload bill/invoice file
|
||||||
|
Future<Map<String, dynamic>> uploadBill({
|
||||||
|
required String filePath,
|
||||||
|
required String orderId,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,15 +11,18 @@
|
|||||||
library;
|
library;
|
||||||
|
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:io';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
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:image_picker/image_picker.dart';
|
||||||
import 'package:qr_flutter/qr_flutter.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/router/app_router.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';
|
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 remainingSeconds = useState<int>(900);
|
||||||
final timer = useRef<Timer?>(null);
|
final timer = useRef<Timer?>(null);
|
||||||
|
|
||||||
|
// Upload state
|
||||||
|
final isUploadingBill = useState<bool>(false);
|
||||||
|
final selectedImagePath = useState<String?>(null);
|
||||||
|
|
||||||
// Fetch QR code data
|
// Fetch QR code data
|
||||||
useEffect(() {
|
useEffect(() {
|
||||||
Future<void> fetchQrCode() async {
|
Future<void> fetchQrCode() async {
|
||||||
@@ -146,8 +153,27 @@ class PaymentQrPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
const SizedBox(height: AppSpacing.md),
|
const SizedBox(height: AppSpacing.md),
|
||||||
|
|
||||||
|
// Image Preview Section
|
||||||
|
_buildImagePreviewSection(
|
||||||
|
context,
|
||||||
|
selectedImagePath.value,
|
||||||
|
() async {
|
||||||
|
await _selectImage(context, selectedImagePath);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: AppSpacing.md),
|
||||||
|
|
||||||
// Action Buttons
|
// Action Buttons
|
||||||
_buildActionButtons(context),
|
_buildActionButtons(
|
||||||
|
context,
|
||||||
|
ref,
|
||||||
|
isUploadingBill.value,
|
||||||
|
selectedImagePath.value != null,
|
||||||
|
() async {
|
||||||
|
await _uploadBill(context, ref, selectedImagePath, isUploadingBill);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
|
||||||
const SizedBox(height: AppSpacing.md),
|
const SizedBox(height: AppSpacing.md),
|
||||||
|
|
||||||
@@ -452,44 +478,174 @@ class PaymentQrPage extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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: [
|
||||||
|
const Text(
|
||||||
|
'Ảnh hóa đơn',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Color(0xFF212121),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: AppSpacing.md),
|
||||||
|
|
||||||
|
// 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
|
/// Build action buttons
|
||||||
Widget _buildActionButtons(BuildContext context) {
|
Widget _buildActionButtons(
|
||||||
|
BuildContext context,
|
||||||
|
WidgetRef ref,
|
||||||
|
bool isUploading,
|
||||||
|
bool hasImage,
|
||||||
|
VoidCallback onUpload,
|
||||||
|
) {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
|
padding: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
|
||||||
child: Row(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
// Confirmed Payment Button
|
// Upload Bill Button
|
||||||
Expanded(
|
SizedBox(
|
||||||
child: OutlinedButton.icon(
|
width: double.infinity,
|
||||||
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 SizedBox(width: AppSpacing.sm),
|
|
||||||
|
|
||||||
// Upload Proof Button
|
|
||||||
Expanded(
|
|
||||||
child: ElevatedButton.icon(
|
child: ElevatedButton.icon(
|
||||||
onPressed: () => _uploadProof(context),
|
onPressed: (isUploading || !hasImage) ? null : onUpload,
|
||||||
icon: const FaIcon(FontAwesomeIcons.camera, size: 18),
|
icon: isUploading
|
||||||
label: const Text(
|
? const SizedBox(
|
||||||
'Upload bill',
|
width: 18,
|
||||||
style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600),
|
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(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: AppColors.primaryBlue,
|
backgroundColor: AppColors.primaryBlue,
|
||||||
@@ -499,6 +655,33 @@ class PaymentQrPage extends HookConsumerWidget {
|
|||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(AppRadius.button),
|
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
|
/// Select image for bill
|
||||||
void _confirmPayment(BuildContext context) {
|
Future<void> _selectImage(
|
||||||
showDialog<void>(
|
BuildContext context,
|
||||||
|
ValueNotifier<String?> selectedImagePath,
|
||||||
|
) async {
|
||||||
|
// Show bottom sheet to select camera or gallery
|
||||||
|
final ImageSource? source = await showModalBottomSheet<ImageSource>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => AlertDialog(
|
shape: const RoundedRectangleBorder(
|
||||||
title: const Text('Xác nhận thanh toán'),
|
borderRadius: BorderRadius.vertical(top: Radius.circular(AppRadius.card)),
|
||||||
content: const Text(
|
|
||||||
'Bạn đã hoàn tất thanh toán cho đơn hàng này?',
|
|
||||||
style: TextStyle(fontSize: 14),
|
|
||||||
),
|
),
|
||||||
actions: [
|
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(
|
TextButton(
|
||||||
onPressed: () => Navigator.of(context).pop(),
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
child: const Text('Chưa'),
|
child: const Text('Hủy'),
|
||||||
),
|
),
|
||||||
ElevatedButton(
|
],
|
||||||
onPressed: () {
|
),
|
||||||
Navigator.of(context).pop();
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
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(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(
|
const SnackBar(
|
||||||
content: Text('Đã xác nhận thanh toán!'),
|
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,
|
backgroundColor: AppColors.success,
|
||||||
duration: Duration(seconds: 2),
|
duration: Duration(seconds: 2),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
// Navigate back after delay
|
} catch (e) {
|
||||||
Future.delayed(const Duration(milliseconds: 500), () {
|
if (!context.mounted) return;
|
||||||
if (context.mounted) {
|
|
||||||
context.pop();
|
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 upload'),
|
||||||
|
content: const Text(
|
||||||
|
'Bạn có muốn upload hóa đơn này không?',
|
||||||
|
style: TextStyle(fontSize: 14),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(false),
|
||||||
|
child: const Text('Hủy'),
|
||||||
|
),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(true),
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: AppColors.primaryBlue,
|
backgroundColor: AppColors.primaryBlue,
|
||||||
),
|
),
|
||||||
child: const Text('Đã thanh toán'),
|
child: const Text('Upload'),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
/// Upload payment proof
|
if (confirmed != true || !context.mounted) return;
|
||||||
void _uploadProof(BuildContext context) {
|
|
||||||
// TODO: Implement image picker and upload
|
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(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(
|
const SnackBar(
|
||||||
content: Text('Tính năng upload bill đang được phát triển'),
|
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),
|
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
|
/// Format currency
|
||||||
|
|||||||
Reference in New Issue
Block a user