Merge branch 'refs/heads/export_module' into export_module_ui_ux_enhancement

main_production_RSG
Sikander Saleem 2 months ago
commit 8864e122bc

@ -197,6 +197,40 @@ class ApiManager {
return streamedResponse; return streamedResponse;
} }
/// POST FormData request that returns StreamedResponse for progress tracking
/// This method is specifically for form-data requests (multipart/form-data)
Future<http.StreamedResponse> postFormDataStreaming(String url, {Map<String, String>? headers, required Map<String, String> body}) async {
// Create headers for form data
Map<String, String> formHeaders = {
'X-Timezone-Offset': DateTime.now().timeZoneOffset.toString().split(".").first,
if (user != null) 'Authorization': 'Bearer ${user!.token}',
if (assetGroup != null) 'AssetGroup': assetGroup!.id.toString(),
};
headers ??= {};
headers.addAll(formHeaders);
Uri url0 = Uri.parse(url);
if (!kReleaseMode) {
print("URL:$url0");
print("Headers:$headers");
print("FormData Body:$body");
}
var request = http.MultipartRequest('POST', url0);
request.fields.addAll(body);
request.headers.addAll(headers);
http.StreamedResponse streamedResponse = await _httpClient.send(request);
// Handle 401 immediately
if (streamedResponse.statusCode == 401) {
showLoginDialog();
}
return streamedResponse;
}
Future<http.Response> delete(String url, {Map<String, String>? headers, required Map<String, dynamic> body}) async { Future<http.Response> delete(String url, {Map<String, String>? headers, required Map<String, dynamic> body}) async {
headers ??= {}; headers ??= {};

@ -556,5 +556,9 @@ class URLs {
static get getAllWorkOrderColumnsMetadata => "$_baseUrl/CallRequest/columns/metadata"; static get getAllWorkOrderColumnsMetadata => "$_baseUrl/CallRequest/columns/metadata";
static get exportAllWorkOrder => "$_baseUrl/CallRequest/Export"; static get exportAllWorkOrder => "$_baseUrl/CallRequest/Export";
// Print/Report URLs
static get generateReport => "$_baseUrl/ReportPDF/GenerateReport";
//4104 attachment type lookup //4104 attachment type lookup
} }

@ -1,9 +1,11 @@
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:test_sa/controllers/providers/api/user_provider.dart'; import 'package:test_sa/controllers/providers/api/user_provider.dart';
import 'package:test_sa/extensions/context_extension.dart'; import 'package:test_sa/extensions/context_extension.dart';
import 'package:test_sa/extensions/int_extensions.dart'; import 'package:test_sa/extensions/int_extensions.dart';
import 'package:test_sa/extensions/string_extensions.dart';
import 'package:test_sa/extensions/text_extensions.dart'; import 'package:test_sa/extensions/text_extensions.dart';
import 'package:test_sa/extensions/widget_extensions.dart'; import 'package:test_sa/extensions/widget_extensions.dart';
import 'package:test_sa/helper/utils.dart'; import 'package:test_sa/helper/utils.dart';
@ -12,16 +14,18 @@ import 'package:test_sa/models/enums/work_order_next_step.dart';
import 'package:test_sa/models/helper_data_models/workorder/work_order_helper_models.dart'; import 'package:test_sa/models/helper_data_models/workorder/work_order_helper_models.dart';
import 'package:test_sa/modules/cm_module/cm_detail_provider.dart'; import 'package:test_sa/modules/cm_module/cm_detail_provider.dart';
import 'package:test_sa/modules/cm_module/views/components/bottom_sheets/service_request_bottomsheet.dart'; import 'package:test_sa/modules/cm_module/views/components/bottom_sheets/service_request_bottomsheet.dart';
import 'package:test_sa/modules/cx_module/chat/chat_page.dart';
import 'package:test_sa/modules/cx_module/chat/chat_provider.dart'; import 'package:test_sa/modules/cx_module/chat/chat_provider.dart';
import 'package:test_sa/modules/cx_module/chat/chat_widget.dart'; import 'package:test_sa/modules/cx_module/chat/chat_widget.dart';
import 'package:test_sa/modules/cx_module/survey/survey_page.dart'; import 'package:test_sa/modules/export_module/provider/export_provider.dart';
import 'package:test_sa/modules/loan_module/pages/create_loan_request_page.dart'; import 'package:test_sa/modules/loan_module/pages/create_loan_request_page.dart';
import 'package:test_sa/modules/loan_module/pages/loan_equipment_detail_page.dart'; import 'package:test_sa/modules/loan_module/pages/loan_equipment_detail_page.dart';
import 'package:test_sa/modules/loan_module/provider/loan_provider.dart'; import 'package:test_sa/modules/loan_module/provider/loan_provider.dart';
import 'package:test_sa/new_views/app_style/app_color.dart'; import 'package:test_sa/new_views/app_style/app_color.dart';
import 'package:test_sa/new_views/common_widgets/app_filled_button.dart';
import 'package:test_sa/new_views/common_widgets/default_app_bar.dart'; import 'package:test_sa/new_views/common_widgets/default_app_bar.dart';
import 'package:test_sa/views/widgets/loaders/no_data_found.dart'; import 'package:open_file/open_file.dart';
import 'package:share_plus/share_plus.dart';
import 'dart:developer';
import 'views/components/history_log_view.dart'; import 'views/components/history_log_view.dart';
import 'views/components/service_request_detail_view.dart'; import 'views/components/service_request_detail_view.dart';
@ -170,6 +174,11 @@ class _CMDetailPageState extends State<CMDetailPage> {
return const SizedBox(); return const SizedBox();
} }
}), }),
// Print Button
IconButton(
icon: const Icon(Icons.print),
onPressed: _handlePrint,
),
IconButton( IconButton(
icon: const Icon(Icons.home), icon: const Icon(Icons.home),
onPressed: () { onPressed: () {
@ -245,4 +254,138 @@ class _CMDetailPageState extends State<CMDetailPage> {
cProvider.getUserAutoLoginTokenSilent(moduleId, widget.requestId, title, myEmployeeId, receiver); cProvider.getUserAutoLoginTokenSilent(moduleId, widget.requestId, title, myEmployeeId, receiver);
} }
/// Show success dialog after file is downloaded - using same implementation as export module
Future<void> _showSuccessDialog(String filePath) async {
if (!mounted) return;
return showDialog(
context: context,
barrierDismissible: false,
builder: (BuildContext dialogContext) {
return Dialog(
backgroundColor: AppColor.background(context),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SvgPicture.asset(
'assets/images/done.svg',
width: 80,
height: 80,
),
const SizedBox(height: 24),
Text(
'Print Successful',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: AppColor.headingTextColor(context),
),
textAlign: TextAlign.center,
),
const SizedBox(height: 12),
Text(
'Your PDF has been generated successfully',
style: TextStyle(
fontSize: 14,
color: AppColor.lightTextColor(context),
),
textAlign: TextAlign.center,
),
const SizedBox(height: 32),
Row(
children: [
Expanded(
child: AppFilledButton(
onPressed: () async {
Navigator.of(dialogContext).pop();
log('📂 Opening printed file...');
final result = await OpenFile.open(filePath);
if (result.type != ResultType.done) {
log('⚠️ Could not open file: ${result.message}');
'No app found to open this file type. File saved to: $filePath'.showToast;
}
},
label: 'Open',
buttonColor: AppColor.primary10,
showIcon: true,
icon: const Icon(Icons.folder_open, color: Colors.white),
height: 48,
),
),
const SizedBox(width: 12),
Expanded(
child: AppFilledButton(
onPressed: () async {
Navigator.of(dialogContext).pop();
log('📤 Sharing printed file...');
try {
final xFile = XFile(filePath);
await Share.shareXFiles(
[xFile],
text: 'Printed file',
);
log('✅ File shared successfully');
} catch (e) {
log('❌ Error sharing file: $e');
'Failed to share file: $e'.showToast;
}
},
label: 'Share',
buttonColor: AppColor.green40,
textColor: AppColor.green60,
showIcon: true,
icon: Icon(Icons.share, color: AppColor.green60),
height: 48,
),
),
],
),
],
),
),
);
},
);
}
/// Handle print button press
Future<void> _handlePrint() async {
try {
final exportProvider = Provider.of<ExportApiProvider>(context, listen: false);
final filePath = await exportProvider.printWorkOrder(
widget.requestId,
widget.moduleId,
context,
);
if (filePath != null) {
await _showSuccessDialog(filePath);
} else if (exportProvider.errorMessage != null) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(exportProvider.errorMessage!),
backgroundColor: AppColor.red60,
),
);
}
}
} catch (error) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Print failed: $error'),
backgroundColor: AppColor.red60,
),
);
}
}
}
} }

@ -240,4 +240,155 @@ class ExportApiProvider extends ChangeNotifier {
_errorMessage = null; _errorMessage = null;
notifyListeners(); notifyListeners();
} }
/// Print work order data as PDF
Future<String?> printWorkOrder(int workOrderId, int moduleId, BuildContext context) async {
_isExporting = true;
_errorMessage = null;
notifyListeners();
ProgressDialog pd = ProgressDialog(context: context);
pd.show(
max: 100,
msg: 'Preparing print...',
progressBgColor: Colors.transparent,
progressValueColor: AppColor.primary10,
msgFontWeight: FontWeight.normal,
valueColor: AppColor.primary10,
);
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<String, String> 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');
}
}
} }

Loading…
Cancel
Save