From 07fd3f9ba716f3434c1edff80b769eacd35cd943 Mon Sep 17 00:00:00 2001 From: Hamza-Ayed Date: Fri, 8 May 2026 15:18:01 +0300 Subject: [PATCH] Update: 2026-05-08 15:18:01 --- app/Services/InvoiceProcessor.php | 19 ++++-- app/Services/NotificationService.php | 9 ++- .../controllers/scanner_controller.dart | 65 ++++++++++++++++++- 3 files changed, 85 insertions(+), 8 deletions(-) diff --git a/app/Services/InvoiceProcessor.php b/app/Services/InvoiceProcessor.php index 406d3b7..f90fb5e 100644 --- a/app/Services/InvoiceProcessor.php +++ b/app/Services/InvoiceProcessor.php @@ -155,16 +155,18 @@ class InvoiceProcessor // Check if entire batch is complete self::checkBatchCompletion($batchId); - // Send push notification (non-critical, don't fail on error) + // Progress/Completion Push try { $stmt = $db->prepare("SELECT total_images, processed_images, uploaded_by FROM invoice_batches WHERE id = ?"); $stmt->execute([$batchId]); $currentBatch = $stmt->fetch(); if ($currentBatch) { $notifier = new NotificationService(); + // Send data notification with invoice_id for auto-navigation $notifier->sendDataNotification($currentBatch['uploaded_by'], [ - 'type' => 'batch_progress', + 'type' => 'invoice_processed', 'batch_id' => $batchId, + 'invoice_id' => $invoiceId, 'processed' => $currentBatch['processed_images'], 'total' => $currentBatch['total_images'] ]); @@ -194,12 +196,21 @@ class InvoiceProcessor self::log("Batch $batchId: COMPLETE ({$batch['processed_images']}/{$batch['total_images']})"); try { + // Try to get the last invoice_id for this batch for completion navigation + $invStmt = $db->prepare("SELECT id FROM invoices WHERE original_file_path IN (SELECT image_path FROM invoice_processing_queue WHERE batch_id = ?) ORDER BY created_at DESC LIMIT 1"); + $invStmt->execute([$batchId]); + $lastInvoiceId = $invStmt->fetchColumn(); + $notifier = new NotificationService(); $notifier->sendNotification( $batch['uploaded_by'], "اكتملت معالجة الدفعة", - "تمت معالجة جميع الفواتير بنجاح. يمكنك الآن مراجعتها وتدقيقها في لوحة التحكم قبل اعتمادها.", - ['batch_id' => $batchId] + "تمت معالجة جميع الفواتير بنجاح. يمكنك الآن مراجعتها وتدقيقها.", + [ + 'type' => 'batch_complete', + 'batch_id' => $batchId, + 'invoice_id' => $lastInvoiceId ?: '' + ] ); } catch (\Throwable $e) { self::log("Batch $batchId: Completion notification failed: " . $e->getMessage()); diff --git a/app/Services/NotificationService.php b/app/Services/NotificationService.php index 2b8b611..a80d628 100644 --- a/app/Services/NotificationService.php +++ b/app/Services/NotificationService.php @@ -17,8 +17,15 @@ class NotificationService public function __construct() { - $this->projectId = env('FIREBASE_PROJECT_ID', ''); $this->serviceAccountPath = env('FIREBASE_SERVICE_ACCOUNT_PATH', APP_PATH . '/config/firebase-service-account.json'); + + // Auto-detect Project ID from Service Account JSON to prevent RESOURCE_PROJECT_INVALID + if (file_exists($this->serviceAccountPath)) { + $sa = json_decode(file_get_contents($this->serviceAccountPath), true); + $this->projectId = $sa['project_id'] ?? env('FIREBASE_PROJECT_ID', ''); + } else { + $this->projectId = env('FIREBASE_PROJECT_ID', ''); + } } /** diff --git a/musadaq-app/lib/features/scanner/controllers/scanner_controller.dart b/musadaq-app/lib/features/scanner/controllers/scanner_controller.dart index 771bc01..5fc80dc 100644 --- a/musadaq-app/lib/features/scanner/controllers/scanner_controller.dart +++ b/musadaq-app/lib/features/scanner/controllers/scanner_controller.dart @@ -38,8 +38,12 @@ class ScannerController extends GetxController { void _initFcmListener() { FirebaseMessaging.onMessage.listen((RemoteMessage message) { final data = message.data; - if (data['type'] == 'batch_progress' && - data['batch_id'] == currentBatchId.value) { + 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; @@ -48,9 +52,21 @@ class ScannerController extends GetxController { _progressService.updateProcessingProgress( processedImagesCount.value, totalImagesCount.value); - if (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']); } } }); @@ -137,6 +153,9 @@ class ScannerController extends GetxController { 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('خطأ', 'فشل رفع الفواتير، يرجى المحاولة لاحقاً'); @@ -150,6 +169,46 @@ class ScannerController extends GetxController { } } + void _startPolling(String batchId) { + // Check status every 2 seconds + Future.doWhile(() async { + // 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); + } + + await Future.delayed(const Duration(seconds: 2)); + return true; // Continue polling + }); + } + void selectCompany(String id, String name) { selectedCompanyId.value = id; selectedCompanyName.value = name;