import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:dio/dio.dart'; import 'package:geolocator/geolocator.dart'; import '../../models/document_model.dart'; import '../../core/location/location_service.dart'; import '../../core/network/api_client.dart'; import '../../config/app_config.dart'; enum PendingDocumentStatus { uploading, processing, success, error } class PendingDocument { final String id; final String imagePath; final PendingDocumentStatus status; final DocumentModel? document; final double? latitude; final double? longitude; final String? errorMessage; final DateTime createdAt; PendingDocument({ required this.id, required this.imagePath, this.status = PendingDocumentStatus.uploading, this.document, this.latitude, this.longitude, this.errorMessage, DateTime? createdAt, }) : createdAt = createdAt ?? DateTime.now(); PendingDocument copyWith({ PendingDocumentStatus? status, DocumentModel? document, double? latitude, double? longitude, String? errorMessage, }) { return PendingDocument( id: id, imagePath: imagePath, status: status ?? this.status, document: document ?? this.document, latitude: latitude ?? this.latitude, longitude: longitude ?? this.longitude, errorMessage: errorMessage, createdAt: createdAt, ); } } class PendingDocumentsNotifier extends StateNotifier> { PendingDocumentsNotifier() : super([]); void addDocument(String imagePath, {double? latitude, double? longitude}) { final id = DateTime.now().millisecondsSinceEpoch.toString(); final newDoc = PendingDocument( id: id, imagePath: imagePath, latitude: latitude, longitude: longitude, ); state = [newDoc, ...state]; _uploadAndProcess(id, imagePath, latitude: latitude, longitude: longitude); } Future _determinePosition() async { return await LocationService.determinePosition(); } 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 _determinePosition(); if (position != null) { lat = position.latitude; lng = position.longitude; if (mounted) { state = [ for (final item in state) if (item.id == id) item.copyWith(latitude: lat, longitude: lng) else item ]; } } } catch (_) { // Gracefully continue without location tags if fetch fails } } try { // API call to upload and process final apiClient = ApiClient(); 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(), }); final response = await apiClient.client.post( AppConfig.uploadDocumentEndpoint, data: formData, ); final doc = DocumentModel.fromJson(response.data['data']); if (!mounted) return; state = [ for (final item in state) if (item.id == id) item.copyWith(status: PendingDocumentStatus.processing, document: doc) else item ]; // Poll until the document is parsed on the server bool isParsed = false; DocumentModel? parsedDoc; int retries = 0; const maxRetries = 60; // 2 minutes max polling (60 retries * 2s) while (!isParsed && retries < maxRetries && mounted) { await Future.delayed(const Duration(seconds: 2)); if (!mounted) return; try { final getRes = await apiClient.client.get(AppConfig.fetchDocumentsEndpoint); if (getRes.statusCode == 200 && getRes.data['data'] is List) { final list = (getRes.data['data'] as List) .map((e) => DocumentModel.fromJson(Map.from(e))) .toList(); final found = list.firstWhere( (element) => element.id == doc.id, orElse: () => doc, ); if (found != doc) { isParsed = true; parsedDoc = found; } } } catch (_) { // Ignore network glitch during polling } retries++; } if (!mounted) return; if (isParsed && parsedDoc != null) { state = [ for (final item in state) if (item.id == id) item.copyWith(status: PendingDocumentStatus.success, document: parsedDoc) else item ]; } else { state = [ for (final item in state) if (item.id == id) item.copyWith( status: PendingDocumentStatus.error, errorMessage: 'Gagal mengekstrak data dari dokumen (Timeout).', ) else item ]; } } catch (e) { if (!mounted) return; state = [ for (final item in state) if (item.id == id) item.copyWith(status: PendingDocumentStatus.error, errorMessage: e.toString()) else item ]; } } void removeDocument(String id) { state = state.where((item) => item.id != id).toList(); } void retryUpload(String id) { final doc = state.firstWhere((item) => item.id == id); final lat = doc.latitude; final lng = doc.longitude; state = [ for (final item in state) if (item.id == id) item.copyWith( status: PendingDocumentStatus.uploading, errorMessage: null, ) else item ]; _uploadAndProcess(id, doc.imagePath, latitude: lat, longitude: lng); } } final pendingDocumentsProvider = StateNotifierProvider>((ref) { return PendingDocumentsNotifier(); });