import 'dart:io'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:path_provider/path_provider.dart'; import 'package:path/path.dart' as path; import 'package:file_picker/file_picker.dart'; import '../../../core/services/upload_progress_service.dart'; import '../../../core/utils/logger.dart'; import '../../../core/utils/app_snackbar.dart'; import '../../../core/services/image_processing_service.dart'; import '../../../core/services/invoice_upload_service.dart'; import '../../../core/network/dio_client.dart'; class ScannerController extends GetxController { var capturedImages = [].obs; var isProcessing = false.obs; var uploadProgress = 0.0.obs; var companies = >[].obs; var isLoadingCompanies = false.obs; var currentBatchId = ''.obs; var processedImagesCount = 0.obs; var totalImagesCount = 0.obs; var isBatchDone = false.obs; var selectedCompanyId = ''.obs; var selectedCompanyName = ''.obs; final InvoiceUploadService _uploadService = InvoiceUploadService(); final UploadProgressService _progressService = Get.find(); @override void onInit() { super.onInit(); fetchCompanies(); _initFcmListener(); } void _initFcmListener() { FirebaseMessaging.onMessage.listen((RemoteMessage message) { final data = message.data; final type = data['type']; final batchId = data['batch_id']; if (batchId != currentBatchId.value) return; if (type == 'invoice_processed' || type == 'batch_progress') { processedImagesCount.value = int.tryParse(data['processed'].toString()) ?? 0; totalImagesCount.value = int.tryParse(data['total'].toString()) ?? 0; // Update global progress service _progressService.updateProcessingProgress( processedImagesCount.value, totalImagesCount.value); // If it's a single invoice, we can navigate directly if (totalImagesCount.value == 1 && data['invoice_id'] != null) { isBatchDone.value = true; _progressService.complete(); // Open invoice details Get.toNamed('/invoice-detail', arguments: data['invoice_id']); } } else if (type == 'batch_complete') { isBatchDone.value = true; _progressService.complete(); // Optionally navigate to invoices list or specific invoice if (data['invoice_id'] != null && data['invoice_id'].toString().isNotEmpty) { Get.toNamed('/invoice-detail', arguments: data['invoice_id']); } } }); } Future fetchCompanies() async { isLoadingCompanies.value = true; try { final res = await DioClient().client.get('companies'); if (res.data['success'] == true && res.data['data'] != null) { companies.value = List>.from(res.data['data']); } } catch (e) { AppLogger.error('Failed to fetch companies', e); } finally { isLoadingCompanies.value = false; } } Future addImage(String imagePath) async { File originalFile = File(imagePath); capturedImages.add(originalFile); int index = capturedImages.length - 1; if (imagePath.toLowerCase().endsWith('.pdf')) { AppLogger.print('Added PDF file, skipping image processing: $imagePath'); return; } ImageProcessingService.processInvoiceImage(originalFile) .then((processedFile) { if (processedFile != null && index < capturedImages.length) { capturedImages[index] = processedFile; AppLogger.print('Finished processing image in background.'); } }).catchError((e) { AppLogger.error('Failed to process image in background', e); }); } Future pickPdfFile() async { try { FilePickerResult? result = await FilePicker.platform.pickFiles( type: FileType.custom, allowedExtensions: ['pdf'], allowMultiple: true, ); if (result != null) { for (var file in result.files) { if (file.path != null) { addImage(file.path!); } } AppSnackbar.showSuccess('تمت الإضافة', 'تم استيراد ملفات الفواتير بنجاح'); } } catch (e) { AppLogger.error('Failed to pick PDF', e); AppSnackbar.showError('خطأ', 'تعذر استيراد الملفات'); } } void removeImage(int index) { if (index >= 0 && index < capturedImages.length) { capturedImages.removeAt(index); } } Future uploadBatch() async { if (capturedImages.isEmpty) { AppSnackbar.showWarning('تنبيه', 'الرجاء تصوير فاتورة واحدة على الأقل'); return; } if (selectedCompanyId.isEmpty) { AppSnackbar.showWarning('تنبيه', 'الرجاء اختيار الشركة أولاً'); return; } try { isProcessing.value = true; uploadProgress.value = 0.0; AppLogger.print( 'Uploading batch of ${capturedImages.length} images to company ${selectedCompanyId.value}...'); // Start global progress _progressService.startUpload(selectedCompanyName.value, capturedImages.length); final batchId = await _uploadService.uploadBatch( companyId: selectedCompanyId.value, images: capturedImages, onProgress: (current, total) { uploadProgress.value = current / total; _progressService.updateProgress(uploadProgress.value, current); }, ); if (batchId != null) { currentBatchId.value = batchId; totalImagesCount.value = capturedImages.length; processedImagesCount.value = 0; // Clear scanner state and go back to dashboard capturedImages.clear(); uploadProgress.value = 0.0; isProcessing.value = false; selectedCompanyId.value = ''; selectedCompanyName.value = ''; _progressService.startProcessing(); Get.back(); // Go back to dashboard, progress will show in overlay AppSnackbar.showSuccess( 'تم البدء', 'تم رفع الصور بنجاح، جاري استخراج البيانات في الخلفية'); // Start polling for status (Reliable fallback for FCM) _startPolling(batchId); } else { _progressService.fail(); AppSnackbar.showError('خطأ', 'فشل رفع الفواتير، يرجى المحاولة لاحقاً'); } } catch (e) { _progressService.fail(); AppLogger.error('Failed to upload batch', e); AppSnackbar.showError('خطأ', 'حدث خطأ غير متوقع أثناء الرفع'); } finally { isProcessing.value = false; } } void _startPolling(String batchId) { bool firstPoll = true; // Check status periodically Future.doWhile(() async { // Wait before checking // First poll is after 8 seconds (AI takes time), subsequent are 5 seconds await Future.delayed(Duration(seconds: firstPoll ? 8 : 5)); firstPoll = false; // If we are no longer interested in this batch or it's done, stop polling if (currentBatchId.value != batchId || isBatchDone.value) return false; try { final res = await DioClient().client.get('batches/status', queryParameters: {'batch_id': batchId}); if (res.data['success'] == true) { final batch = res.data['data']['batch']; final items = res.data['data']['items'] as List; processedImagesCount.value = int.tryParse(batch['processed_images'].toString()) ?? 0; totalImagesCount.value = int.tryParse(batch['total_images'].toString()) ?? 1; _progressService.updateProcessingProgress(processedImagesCount.value, totalImagesCount.value); if (batch['status'] == 'done') { isBatchDone.value = true; _progressService.complete(); // If it's a single invoice, find the invoice_id and navigate if (totalImagesCount.value == 1 && items.isNotEmpty) { final invoiceId = items.first['invoice_id']; if (invoiceId != null) { Get.toNamed('/invoice-detail', arguments: invoiceId); } } return false; // Stop polling } } } catch (e) { AppLogger.error('Polling error', e); } return true; // Continue polling }); } void selectCompany(String id, String name) { selectedCompanyId.value = id; selectedCompanyName.value = name; } Future getSavePath() async { final directory = await getTemporaryDirectory(); final fileName = 'invoice_${DateTime.now().millisecondsSinceEpoch}.jpg'; return path.join(directory.path, fileName); } }