export 'pending_document_model.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:dio/dio.dart'; import '../../models/document_model.dart'; import '../../core/location/location_service.dart'; import '../../core/network/api_client.dart'; import '../../core/network/api_exception.dart'; import '../../config/app_config.dart'; import '../../main.dart'; import '../camera/scan_mode_provider.dart'; import 'pending_document_model.dart'; import 'poll_outcome.dart'; class PendingDocumentsNotifier extends StateNotifier> { PendingDocumentsNotifier(this._ref) : super([]) { _loadPendingDocuments(); } final Ref _ref; void _loadPendingDocuments() { final localStorage = _ref.read(localStorageProvider); final list = localStorage.getAllPendingDocuments() .map((e) => PendingDocument.fromJson(e)) .toList(); state = list; for (var doc in list) { if (doc.status == PendingDocumentStatus.processing && doc.document != null) { _resumePolling(doc.id, doc.document!); } else if (doc.status == PendingDocumentStatus.uploading) { _uploadAndProcess(doc.id, doc.imagePath, latitude: doc.latitude, longitude: doc.longitude); } } } void _persistPendingDocument(PendingDocument doc) { _ref.read(localStorageProvider).savePendingDocument(doc.toJson()); } void _removePersistedPendingDocument(String id) { _ref.read(localStorageProvider).removePendingDocument(id); } void _updateItem(String id, PendingDocument Function(PendingDocument) updateFn) { state = [for (var item in state) if (item.id == id) updateFn(item) else item]; _persistPendingDocument(state.firstWhere((item) => item.id == id)); } void _updateItemInMemory(String id, PendingDocument Function(PendingDocument) updateFn) { state = [for (var item in state) if (item.id == id) updateFn(item) else item]; } void addDocument(String imagePath, {double? latitude, double? longitude}) { final id = DateTime.now().millisecondsSinceEpoch.toString(); final scanMode = _ref.read(scanModeProvider); final newDoc = PendingDocument( id: id, imagePath: imagePath, latitude: latitude, longitude: longitude, scanMode: scanMode, ); state = [newDoc, ...state]; _persistPendingDocument(newDoc); _uploadAndProcess(id, imagePath, latitude: latitude, longitude: longitude); } Future _uploadAndProcess(String id, String imagePath, {double? latitude, double? longitude}) async { double? lat = latitude; double? lng = longitude; if (lat == null || lng == null) { try { final position = await LocationService.determinePosition(); if (position != null) { lat = position.latitude; lng = position.longitude; if (mounted) _updateItem(id, (item) => item.copyWith(latitude: lat, longitude: lng)); } } catch (_) {} } try { final scanMode = _ref.read(scanModeProvider); final apiClient = _ref.read(apiClientProvider); final formData = FormData.fromMap({ 'image': await MultipartFile.fromFile(imagePath, filename: imagePath.split(RegExp(r'[/\\]')).last), if (lat != null) 'latitude': lat.toString(), if (lng != null) 'longitude': lng.toString(), 'scan_mode': scanMode, }); final response = await apiClient.client.post(AppConfig.uploadDocumentEndpoint, data: formData); final doc = DocumentModel.fromJson(response.data['data']); if (!mounted) return; _updateItem(id, (item) => item.copyWith(status: PendingDocumentStatus.processing, document: doc)); await _pollUntilParsed(id, doc); } catch (e) { if (!mounted) return; _updateItem(id, (item) => item.copyWith(status: PendingDocumentStatus.error, errorMessage: ApiException.from(e).displayMessage)); } } Future _resumePolling(String id, DocumentModel doc) async { try { await _pollUntilParsed(id, doc); } catch (e) { if (!mounted) return; _updateItem(id, (item) => item.copyWith(status: PendingDocumentStatus.error, errorMessage: ApiException.from(e).displayMessage)); } } Future _pollUntilParsed(String id, DocumentModel doc) async { final apiClient = _ref.read(apiClientProvider); DocumentModel? resultDoc; String? failureMessage; int retries = 0; const maxRetries = 130; while (retries < maxRetries && mounted) { await Future.delayed(const Duration(seconds: 2)); if (!mounted) return; try { final getRes = await apiClient.client.get('${AppConfig.fetchDocumentsEndpoint}/${doc.id}'); if (getRes.statusCode == 200 && getRes.data['data'] != null) { final fetched = DocumentModel.fromJson(Map.from(getRes.data['data'])); final outcome = resolvePollOutcome(fetched); if (outcome.kind == PollOutcomeKind.done) { resultDoc = outcome.document; break; } else if (outcome.kind == PollOutcomeKind.failed) { failureMessage = outcome.message; break; } } } catch (_) {} retries++; } if (!mounted) return; if (resultDoc != null) { _updateItem(id, (item) => item.copyWith(status: PendingDocumentStatus.success, document: resultDoc)); } else { _updateItem(id, (item) => item.copyWith(status: PendingDocumentStatus.error, errorMessage: failureMessage ?? 'Gagal mengekstrak data (Timeout).')); } } void removeDocument(String id) { state = state.where((item) => item.id != id).toList(); _removePersistedPendingDocument(id); } void markSyncFailed(String id, DocumentModel doc, String message) { _updateItem(id, (item) => item.copyWith(status: PendingDocumentStatus.syncFailed, document: doc, errorMessage: message)); } Future retrySync(String id) async { final matches = state.where((item) => item.id == id); if (matches.isEmpty) return; final doc = matches.first.document; if (doc == null) return; _updateItemInMemory(id, (item) => item.copyWith(status: PendingDocumentStatus.uploading, errorMessage: null)); try { final apiClient = _ref.read(apiClientProvider); await apiClient.client.put('${AppConfig.fetchDocumentsEndpoint}/${doc.id}', data: doc.toPutPayload()); if (!mounted) return; removeDocument(id); } catch (e) { if (!mounted) return; _updateItem(id, (item) => item.copyWith( status: PendingDocumentStatus.syncFailed, errorMessage: 'Gagal sinkronisasi ke server: ${ApiException.from(e).displayMessage}', )); } } void retryUpload(String id) { final doc = state.firstWhere((item) => item.id == id); _updateItem(id, (item) => item.copyWith(status: PendingDocumentStatus.uploading, errorMessage: null)); _uploadAndProcess(id, doc.imagePath, latitude: doc.latitude, longitude: doc.longitude); } void clearQueue() { state = const []; _ref.read(localStorageProvider).pendingDocumentsBox.clear(); } } final pendingDocumentsProvider = StateNotifierProvider>((ref) { return PendingDocumentsNotifier(ref); });