import 'dart:convert'; import 'dart:developer'; import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:path_provider/path_provider.dart'; import 'package:sn_progress_dialog/sn_progress_dialog.dart'; import 'package:test_sa/controllers/api_routes/api_manager.dart'; import 'package:test_sa/controllers/api_routes/urls.dart'; import 'package:test_sa/extensions/string_extensions.dart'; import 'package:test_sa/extensions/text_extensions.dart'; import 'package:test_sa/modules/export_module/models/export_form_model.dart'; import 'package:test_sa/modules/export_module/models/export_module_type.dart'; import 'package:test_sa/new_views/app_style/app_color.dart'; import 'package:test_sa/extensions/context_extension.dart'; class ExportApiProvider extends ChangeNotifier { bool _isExporting = false; bool get isExporting => _isExporting; int? _stateCode; int? get stateCode => _stateCode; String? _errorMessage; String? get errorMessage => _errorMessage; Future exportData(ExportRequest exportRequest, ExportModuleType moduleType, BuildContext context) async { _isExporting = true; _errorMessage = null; notifyListeners(); // Capture colors before showing dialog to avoid Provider context issues // Use isDarkNotListen to safely access provider without listening final backgroundColor = context.isDarkNotListen ? AppColor.containerDark : Colors.white; final textColor = context.isDarkNotListen ? AppColor.white10 : AppColor.white936; bool isPdCancel = false; ProgressDialog pd = ProgressDialog(context: context); pd.show( max: 100, msg: 'Preparing export...', progressBgColor: Colors.transparent, progressValueColor: AppColor.primary10, msgFontWeight: FontWeight.normal, msgColor: textColor, msgMaxLines: 2, valueColor: textColor, elevation: 4, msgFontSize: 14, backgroundColor: backgroundColor, cancel: Cancel( cancelClicked: () { isPdCancel = true; 'Export Canceled'.showToast; pd.close(); }, cancelImageColor: AppColor.icon2ColorNotListen(context), ), ); IOSink? sink; File? outputFile; try { final String apiUrl = _getApiUrlForModule(moduleType); final Map body = _buildRequestBody(exportRequest, moduleType); if (!context.mounted) return null; pd.update(msg: 'Sending request to server'); log('📡 Sending export request...'); final startTime = DateTime.now(); final streamedResponse = await ApiManager.instance.postStreaming(apiUrl, body: body); _stateCode = streamedResponse.statusCode; final responseTime = DateTime.now().difference(startTime).inMilliseconds; log('✅ Response received in ${responseTime}ms - Status: ${streamedResponse.statusCode}'); if (streamedResponse.statusCode >= 200 && streamedResponse.statusCode < 300) { final contentLength = streamedResponse.contentLength ?? 0; log('📊 Content length: ${contentLength > 0 ? "${(contentLength / 1024 / 1024).toStringAsFixed(2)} MB" : "Unknown"}'); if (!context.mounted) { await streamedResponse.stream.drain(); return null; } final String ext = _getFileExtension(exportRequest.format); final String dir = (await getApplicationDocumentsDirectory()).path; final String filePath = "$dir/${DateTime.now().millisecondsSinceEpoch}.$ext"; outputFile = File(filePath); sink = outputFile.openWrite(); int received = 0; int lastProgress = 15; final downloadStartTime = DateTime.now(); pd.update(value: 15, msg: 'Exporting...'); await for (var chunk in streamedResponse.stream) { sink.add(chunk); received += chunk.length; if (!context.mounted) { await sink.close(); await outputFile.delete(); return null; } if (contentLength > 0) { final progress = (15 + ((received / contentLength) * 70)).clamp(15, 85).toInt(); if (progress > lastProgress) { lastProgress = progress; final downloadedMB = (received / 1024 / 1024).toStringAsFixed(2); final totalMB = (contentLength / 1024 / 1024).toStringAsFixed(2); pd.update(value: progress, msg: 'Exporting: $downloadedMB MB / $totalMB MB'); } } else { final downloadedMB = (received / 1024 / 1024).toStringAsFixed(2); pd.update(msg: 'Exporting: $downloadedMB MB'); } } await sink.close(); sink = null; final downloadTime = DateTime.now().difference(downloadStartTime).inMilliseconds; final fileSizeMB = (received / 1024 / 1024).toStringAsFixed(2); final downloadSpeed = downloadTime > 0 ? ((received / 1024 / 1024) / (downloadTime / 1000)).toStringAsFixed(2) : 'N/A'; if (!context.mounted) { await outputFile.delete(); return null; } pd.close(); final totalTime = DateTime.now().difference(startTime).inMilliseconds; if (isPdCancel) { return ""; } return filePath; } else { if (context.mounted) { pd.close(); } try { final responseBody = await streamedResponse.stream.bytesToString(); final responseData = json.decode(responseBody); _errorMessage = responseData['message'] ?? 'Export failed'; } catch (e) { log('❌ Error parsing error response: $e'); _errorMessage = 'Export failed with status: ${streamedResponse.statusCode}'; } return null; } } catch (error, stackTrace) { if (sink != null) { try { await sink.close(); } catch (e) { log('⚠️ Error closing sink: $e'); } } if (outputFile != null && await outputFile.exists()) { try { await outputFile.delete(); log('🗑️ Deleted incomplete file'); } catch (e) { log('⚠️ Error deleting incomplete file: $e'); } } if (context.mounted) { pd.close(); } log('❌ Error exporting data: $error'); _stateCode = -1; _errorMessage = 'An error occurred during export: $error'; return null; } finally { _isExporting = false; notifyListeners(); log('🔚 Export process finished'); } } String _getApiUrlForModule(ExportModuleType moduleType) { switch (moduleType) { case ExportModuleType.asset: return URLs.exportAsset; case ExportModuleType.callRequest: return URLs.exportAllWorkOrder; default: return URLs.exportAsset; } } String _getFileExtension(String format) { final formatLower = format.toLowerCase(); switch (formatLower) { case 'excel': return 'xlsx'; case 'pdf': return 'pdf'; case 'word': case 'doc': return 'docx'; case 'text': case 'txt': return 'txt'; case 'csv': return 'csv'; default: return formatLower; } } Map _buildRequestBody(ExportRequest exportRequest, ExportModuleType moduleType) { final Map body = {}; if (exportRequest.searchModel != null) { try { final searchModelJson = (exportRequest.searchModel as dynamic).toJson() as Map; body.addAll(searchModelJson); } catch (e) { body['searchModel'] = exportRequest.searchModel; } } if (moduleType == ExportModuleType.callRequest) { _addCallRequestFields(body); } body['pageNo'] = exportRequest.pageNo; body['pageSize'] = exportRequest.pageSize; body['exportRequest'] = { 'selectedColumns': exportRequest.selectedColumns, 'format': exportRequest.format, }; return body; } void _addCallRequestFields(Map body) { if (!body.containsKey('typeTransaction') || (body['typeTransaction'] as List?)?.isEmpty == true) { body['typeTransaction'] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]; } if (!body.containsKey('statusTransaction') || (body['statusTransaction'] as List?)?.isEmpty == true) { body['statusTransaction'] = [1, 2, 4]; } if (!body.containsKey('priority')) { body['priority'] = []; } if (!body.containsKey('displayData')) { body['displayData'] = []; } } void resetError() { _errorMessage = null; notifyListeners(); } /// Print work order data as PDF Future printWorkOrder(int workOrderId, int moduleId, BuildContext context) async { _isExporting = true; _errorMessage = null; notifyListeners(); // Capture colors before showing dialog to avoid Provider context issues // Use isDarkNotListen to safely access provider without listening final backgroundColor = context.isDarkNotListen ? AppColor.containerDark : Colors.white; final textColor = context.isDarkNotListen ? AppColor.white10 : AppColor.white936; ProgressDialog pd = ProgressDialog(context: context); pd.show( max: 100, msg: 'Preparing print', progressBgColor: Colors.transparent, progressValueColor: AppColor.primary10, msgFontWeight: FontWeight.normal, msgFontSize: 18, elevation: 2, valueColor: textColor, backgroundColor: backgroundColor, msgColor: textColor, ); IOSink? sink; File? outputFile; try { if (!context.mounted) return null; pd.update(msg: 'Sending print request'); log('📡 Sending print request...'); final startTime = DateTime.now(); // Prepare form data final Map formData = { 'properties': json.encode({ 'workOrderId': workOrderId, 'moduleId': moduleId, }), }; final streamedResponse = await ApiManager.instance.postFormDataStreaming(URLs.generateReport, body: formData); _stateCode = streamedResponse.statusCode; final responseTime = DateTime.now().difference(startTime).inMilliseconds; log('✅ Print response received in ${responseTime}ms - Status: ${streamedResponse.statusCode}'); if (streamedResponse.statusCode >= 200 && streamedResponse.statusCode < 300) { final contentLength = streamedResponse.contentLength ?? 0; log('📊 Content length: ${contentLength > 0 ? "${(contentLength / 1024 / 1024).toStringAsFixed(2)} MB" : "Unknown"}'); if (!context.mounted) { await streamedResponse.stream.drain(); return null; } final String dir = (await getApplicationDocumentsDirectory()).path; final String filePath = "$dir/print_${DateTime.now().millisecondsSinceEpoch}.pdf"; outputFile = File(filePath); sink = outputFile.openWrite(); int received = 0; int lastProgress = 15; final downloadStartTime = DateTime.now(); pd.update(value: 15, msg: 'Generating PDF...'); await for (var chunk in streamedResponse.stream) { sink.add(chunk); received += chunk.length; if (!context.mounted) { await sink.close(); await outputFile.delete(); return null; } if (contentLength > 0) { final progress = (15 + ((received / contentLength) * 70)).clamp(15, 85).toInt(); if (progress > lastProgress) { lastProgress = progress; final downloadedMB = (received / 1024 / 1024).toStringAsFixed(2); final totalMB = (contentLength / 1024 / 1024).toStringAsFixed(2); pd.update( value: progress, msg: 'Generating PDF: $downloadedMB MB / $totalMB MB', ); } } else { final downloadedMB = (received / 1024 / 1024).toStringAsFixed(2); pd.update( msg: 'Generating PDF: $downloadedMB MB', ); } } await sink.close(); sink = null; final downloadTime = DateTime.now().difference(downloadStartTime).inMilliseconds; final fileSizeMB = (received / 1024 / 1024).toStringAsFixed(2); log('✅ PDF generated successfully - Size: ${fileSizeMB} MB'); if (!context.mounted) { await outputFile.delete(); return null; } pd.close(); return filePath; } else { if (context.mounted) { pd.close(); } try { final responseBody = await streamedResponse.stream.bytesToString(); final responseData = json.decode(responseBody); _errorMessage = responseData['message'] ?? 'Print failed'; } catch (e) { log('❌ Error parsing print error response: $e'); _errorMessage = 'Print failed with status: ${streamedResponse.statusCode}'; } return null; } } catch (error, stackTrace) { if (sink != null) { try { await sink.close(); } catch (e) { log('⚠️ Error closing sink: $e'); } } if (outputFile != null && await outputFile.exists()) { try { await outputFile.delete(); log('🗑️ Deleted incomplete file'); } catch (e) { log('⚠️ Error deleting incomplete file: $e'); } } if (context.mounted) { pd.close(); } log('❌ Error printing data: $error'); _stateCode = -1; _errorMessage = 'An error occurred during print: $error'; return null; } finally { _isExporting = false; notifyListeners(); log('🔚 Print process finished'); } } }