371 lines
17 KiB
Dart
371 lines
17 KiB
Dart
// lib/services/air_sampling_service.dart
|
|
|
|
import 'dart:io';
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:image_picker/image_picker.dart';
|
|
import 'package:path_provider/path_provider.dart';
|
|
import 'package:path/path.dart' as path;
|
|
import 'package:image/image.dart' as img;
|
|
import 'package:intl/intl.dart';
|
|
import 'dart:convert';
|
|
|
|
import '../models/air_installation_data.dart';
|
|
import '../models/air_collection_data.dart';
|
|
import 'api_service.dart';
|
|
import 'local_storage_service.dart';
|
|
import 'telegram_service.dart';
|
|
import 'server_config_service.dart';
|
|
import 'zipping_service.dart';
|
|
// START CHANGE: Import the new common submission services
|
|
import 'submission_api_service.dart';
|
|
import 'submission_ftp_service.dart';
|
|
// END CHANGE
|
|
|
|
|
|
/// A dedicated service for handling all business logic for the Air Manual Sampling feature.
|
|
class AirSamplingService {
|
|
// START CHANGE: Instantiate new services and remove ApiService
|
|
final DatabaseHelper _dbHelper;
|
|
final TelegramService _telegramService;
|
|
final SubmissionApiService _submissionApiService = SubmissionApiService();
|
|
final SubmissionFtpService _submissionFtpService = SubmissionFtpService();
|
|
final ServerConfigService _serverConfigService = ServerConfigService();
|
|
final ZippingService _zippingService = ZippingService();
|
|
final LocalStorageService _localStorageService = LocalStorageService();
|
|
// END CHANGE
|
|
|
|
// MODIFIED: Constructor no longer needs ApiService
|
|
AirSamplingService(this._dbHelper, this._telegramService);
|
|
|
|
// This helper method remains unchanged as it's for local saving logic
|
|
Map<String, dynamic> _toMapForLocalSave(dynamic data) {
|
|
if (data is AirInstallationData) {
|
|
final map = data.toMap();
|
|
map['imageFront'] = data.imageFront;
|
|
map['imageBack'] = data.imageBack;
|
|
map['imageLeft'] = data.imageLeft;
|
|
map['imageRight'] = data.imageRight;
|
|
map['optionalImage1'] = data.optionalImage1;
|
|
map['optionalImage2'] = data.optionalImage2;
|
|
map['optionalImage3'] = data.optionalImage3;
|
|
map['optionalImage4'] = data.optionalImage4;
|
|
|
|
if (data.collectionData != null) {
|
|
map['collectionData'] = _toMapForLocalSave(data.collectionData);
|
|
}
|
|
return map;
|
|
} else if (data is AirCollectionData) {
|
|
final map = data.toMap();
|
|
map['imageFront'] = data.imageFront;
|
|
map['imageBack'] = data.imageBack;
|
|
map['imageLeft'] = data.imageLeft;
|
|
map['imageRight'] = data.imageRight;
|
|
map['imageChart'] = data.imageChart;
|
|
map['imageFilterPaper'] = data.imageFilterPaper;
|
|
map['optionalImage1'] = data.optionalImage1;
|
|
map['optionalImage2'] = data.optionalImage2;
|
|
map['optionalImage3'] = data.optionalImage3;
|
|
map['optionalImage4'] = data.optionalImage4;
|
|
return map;
|
|
}
|
|
return {};
|
|
}
|
|
|
|
// This image processing utility remains unchanged
|
|
Future<File?> pickAndProcessImage(
|
|
ImageSource source, {
|
|
required String stationCode,
|
|
required String imageInfo,
|
|
String processType = 'INSTALL',
|
|
required bool isRequired,
|
|
}) async {
|
|
final picker = ImagePicker();
|
|
final XFile? photo = await picker.pickImage(
|
|
source: source, imageQuality: 85, maxWidth: 1024);
|
|
if (photo == null) return null;
|
|
|
|
final bytes = await photo.readAsBytes();
|
|
img.Image? originalImage = img.decodeImage(bytes);
|
|
if (originalImage == null) return null;
|
|
|
|
if (isRequired && originalImage.height > originalImage.width) {
|
|
debugPrint("Image orientation check failed: Image must be in landscape mode.");
|
|
return null;
|
|
}
|
|
|
|
final String watermarkTimestamp =
|
|
DateFormat('yyyy-MM-dd HH:mm:ss').format(DateTime.now());
|
|
final font = img.arial24;
|
|
|
|
final textWidth = watermarkTimestamp.length * 12;
|
|
img.fillRect(originalImage,
|
|
x1: 5,
|
|
y1: 5,
|
|
x2: textWidth + 15,
|
|
y2: 35,
|
|
color: img.ColorRgb8(255, 255, 255));
|
|
img.drawString(originalImage, watermarkTimestamp,
|
|
font: font, x: 10, y: 10, color: img.ColorRgb8(0, 0, 0));
|
|
|
|
final tempDir = await getTemporaryDirectory();
|
|
final fileTimestamp = watermarkTimestamp.replaceAll(':', '-').replaceAll(' ', '_');
|
|
|
|
final newFileName =
|
|
"${stationCode}_${fileTimestamp}_${processType.toUpperCase()}_${imageInfo.replaceAll(' ', '')}.jpg";
|
|
|
|
final filePath = path.join(tempDir.path, newFileName);
|
|
|
|
return File(filePath)..writeAsBytesSync(img.encodeJpg(originalImage));
|
|
}
|
|
|
|
// --- REFACTORED submitInstallation method ---
|
|
Future<Map<String, dynamic>> submitInstallation(AirInstallationData data, List<Map<String, dynamic>>? appSettings) async {
|
|
const String moduleName = 'air_installation';
|
|
final activeConfig = await _serverConfigService.getActiveApiConfig();
|
|
final serverName = activeConfig?['config_name'] as String? ?? 'Default';
|
|
|
|
// --- 1. API SUBMISSION (DATA) ---
|
|
debugPrint("Step 1: Delegating Installation Data submission to SubmissionApiService...");
|
|
final dataResult = await _submissionApiService.submitPost(
|
|
moduleName: moduleName,
|
|
endpoint: 'air/manual/installation',
|
|
body: data.toJsonForApi(),
|
|
);
|
|
|
|
if (dataResult['success'] != true) {
|
|
await _logAndSave(data: data, status: 'L1', message: dataResult['message']!, apiResults: [dataResult], ftpStatuses: [], serverName: serverName, type: 'Installation');
|
|
return {'status': 'L1', 'message': dataResult['message']};
|
|
}
|
|
|
|
final recordId = dataResult['data']?['air_man_id']?.toString();
|
|
if (recordId == null) {
|
|
await _logAndSave(data: data, status: 'L1', message: 'API Error: Missing record ID.', apiResults: [dataResult], ftpStatuses: [], serverName: serverName, type: 'Installation');
|
|
return {'status': 'L1', 'message': 'API Error: Missing record ID.'};
|
|
}
|
|
data.airManId = int.tryParse(recordId);
|
|
|
|
// --- 2. API SUBMISSION (IMAGES) ---
|
|
debugPrint("Step 2: Delegating Installation Image submission to SubmissionApiService...");
|
|
final imageFiles = data.getImagesForUpload();
|
|
final imageResult = await _submissionApiService.submitMultipart(
|
|
moduleName: moduleName,
|
|
endpoint: 'air/manual/installation-images',
|
|
fields: {'air_man_id': recordId},
|
|
files: imageFiles,
|
|
);
|
|
final bool apiImagesSuccess = imageResult['success'] == true;
|
|
|
|
// --- 3. FTP SUBMISSION ---
|
|
debugPrint("Step 3: Delegating Installation FTP submission to SubmissionFtpService...");
|
|
final stationCode = data.stationID ?? 'UNKNOWN';
|
|
final samplingDateTime = "${data.installationDate}_${data.installationTime}".replaceAll(':', '-').replaceAll(' ', '_');
|
|
final baseFileName = "${stationCode}_INSTALLATION_${samplingDateTime}";
|
|
|
|
// Zip and submit data
|
|
final dataZip = await _zippingService.createDataZip(jsonDataMap: {'db.json': jsonEncode(data.toDbJson())}, baseFileName: baseFileName);
|
|
Map<String, dynamic> ftpDataResult = {'statuses': []};
|
|
if (dataZip != null) {
|
|
ftpDataResult = await _submissionFtpService.submit(moduleName: moduleName, fileToUpload: dataZip, remotePath: '/air/data/${path.basename(dataZip.path)}');
|
|
}
|
|
|
|
// Zip and submit images
|
|
final imageZip = await _zippingService.createImageZip(imageFiles: imageFiles.values.toList(), baseFileName: baseFileName);
|
|
Map<String, dynamic> ftpImageResult = {'statuses': []};
|
|
if (imageZip != null) {
|
|
ftpImageResult = await _submissionFtpService.submit(moduleName: moduleName, fileToUpload: imageZip, remotePath: '/air/images/${path.basename(imageZip.path)}');
|
|
}
|
|
|
|
final bool ftpSuccess = (ftpDataResult['success'] == true && ftpImageResult['success'] == true);
|
|
|
|
// --- 4. DETERMINE FINAL STATUS, LOG, AND ALERT ---
|
|
String finalStatus;
|
|
String finalMessage;
|
|
if (apiImagesSuccess) {
|
|
finalStatus = ftpSuccess ? 'S4' : 'S2';
|
|
finalMessage = ftpSuccess ? 'Data and files submitted successfully.' : 'Data submitted to API. FTP upload failed or was queued.';
|
|
} else {
|
|
finalStatus = ftpSuccess ? 'L2_FTP_ONLY' : 'L2_PENDING_IMAGES';
|
|
finalMessage = ftpSuccess ? 'API image upload failed, but files were sent via FTP.' : 'Data submitted, but API image and FTP uploads failed.';
|
|
}
|
|
|
|
await _logAndSave(data: data, status: finalStatus, message: finalMessage, apiResults: [dataResult, imageResult], ftpStatuses: [...ftpDataResult['statuses'], ...ftpImageResult['statuses']], serverName: serverName, type: 'Installation');
|
|
_handleInstallationSuccessAlert(data, appSettings, isDataOnly: !apiImagesSuccess);
|
|
|
|
return {'status': finalStatus, 'message': finalMessage};
|
|
}
|
|
|
|
// --- REFACTORED submitCollection method ---
|
|
Future<Map<String, dynamic>> submitCollection(AirCollectionData data, AirInstallationData installationData, List<Map<String, dynamic>>? appSettings) async {
|
|
const String moduleName = 'air_collection';
|
|
final activeConfig = await _serverConfigService.getActiveApiConfig();
|
|
final serverName = activeConfig?['config_name'] as String? ?? 'Default';
|
|
|
|
// --- 1. API SUBMISSION (DATA) ---
|
|
debugPrint("Step 1: Delegating Collection Data submission to SubmissionApiService...");
|
|
data.airManId = installationData.airManId; // Ensure collection is linked to installation
|
|
final dataResult = await _submissionApiService.submitPost(
|
|
moduleName: moduleName,
|
|
endpoint: 'air/manual/collection',
|
|
body: data.toJson(),
|
|
);
|
|
|
|
if (dataResult['success'] != true) {
|
|
await _logAndSave(data: data, installationData: installationData, status: 'L3', message: dataResult['message']!, apiResults: [dataResult], ftpStatuses: [], serverName: serverName, type: 'Collection');
|
|
return {'status': 'L3', 'message': dataResult['message']};
|
|
}
|
|
|
|
// --- 2. API SUBMISSION (IMAGES) ---
|
|
debugPrint("Step 2: Delegating Collection Image submission to SubmissionApiService...");
|
|
final imageFiles = data.getImagesForUpload();
|
|
final imageResult = await _submissionApiService.submitMultipart(
|
|
moduleName: moduleName,
|
|
endpoint: 'air/manual/collection-images',
|
|
fields: {'air_man_id': data.airManId.toString()},
|
|
files: imageFiles,
|
|
);
|
|
final bool apiImagesSuccess = imageResult['success'] == true;
|
|
|
|
// --- 3. FTP SUBMISSION ---
|
|
debugPrint("Step 3: Delegating Collection FTP submission to SubmissionFtpService...");
|
|
final stationCode = installationData.stationID ?? 'UNKNOWN';
|
|
final samplingDateTime = "${data.collectionDate}_${data.collectionTime}".replaceAll(':', '-').replaceAll(' ', '_');
|
|
final baseFileName = "${stationCode}_COLLECTION_${samplingDateTime}";
|
|
|
|
// Zip and submit data (includes both installation and collection data)
|
|
final combinedJson = jsonEncode({"installation": installationData.toDbJson(), "collection": data.toMap()});
|
|
final dataZip = await _zippingService.createDataZip(jsonDataMap: {'db.json': combinedJson}, baseFileName: baseFileName);
|
|
Map<String, dynamic> ftpDataResult = {'statuses': []};
|
|
if (dataZip != null) {
|
|
ftpDataResult = await _submissionFtpService.submit(moduleName: moduleName, fileToUpload: dataZip, remotePath: '/air/data/${path.basename(dataZip.path)}');
|
|
}
|
|
|
|
// Zip and submit images
|
|
final imageZip = await _zippingService.createImageZip(imageFiles: imageFiles.values.toList(), baseFileName: baseFileName);
|
|
Map<String, dynamic> ftpImageResult = {'statuses': []};
|
|
if (imageZip != null) {
|
|
ftpImageResult = await _submissionFtpService.submit(moduleName: moduleName, fileToUpload: imageZip, remotePath: '/air/images/${path.basename(imageZip.path)}');
|
|
}
|
|
|
|
final bool ftpSuccess = (ftpDataResult['success'] == true && ftpImageResult['success'] == true);
|
|
|
|
// --- 4. DETERMINE FINAL STATUS, LOG, AND ALERT ---
|
|
String finalStatus;
|
|
String finalMessage;
|
|
if (apiImagesSuccess) {
|
|
finalStatus = ftpSuccess ? 'S4_API_FTP' : 'S3';
|
|
finalMessage = ftpSuccess ? 'Data and files submitted successfully.' : 'Data submitted to API. FTP upload failed or was queued.';
|
|
} else {
|
|
finalStatus = ftpSuccess ? 'L4_FTP_ONLY' : 'L4_PENDING_IMAGES';
|
|
finalMessage = ftpSuccess ? 'API image upload failed, but files were sent via FTP.' : 'Data submitted, but API image and FTP uploads failed.';
|
|
}
|
|
|
|
await _logAndSave(data: data, installationData: installationData, status: finalStatus, message: finalMessage, apiResults: [dataResult, imageResult], ftpStatuses: [...ftpDataResult['statuses'], ...ftpImageResult['statuses']], serverName: serverName, type: 'Collection');
|
|
_handleCollectionSuccessAlert(data, installationData, appSettings, isDataOnly: !apiImagesSuccess);
|
|
|
|
return {'status': finalStatus, 'message': finalMessage};
|
|
}
|
|
|
|
/// Centralized method for logging and saving data locally.
|
|
Future<void> _logAndSave({
|
|
required dynamic data,
|
|
AirInstallationData? installationData,
|
|
required String status,
|
|
required String message,
|
|
required List<Map<String, dynamic>> apiResults,
|
|
required List<Map<String, dynamic>> ftpStatuses,
|
|
required String serverName,
|
|
required String type,
|
|
}) async {
|
|
String refID;
|
|
Map<String, dynamic> formData;
|
|
List<String> imagePaths;
|
|
|
|
if (type == 'Installation') {
|
|
final installation = data as AirInstallationData;
|
|
installation.status = status;
|
|
refID = installation.refID!;
|
|
formData = installation.toMap();
|
|
imagePaths = _getInstallationImagePaths(installation);
|
|
await _localStorageService.saveAirSamplingRecord(_toMapForLocalSave(installation), refID, serverName: serverName);
|
|
} else {
|
|
final collection = data as AirCollectionData;
|
|
collection.status = status;
|
|
refID = collection.installationRefID!;
|
|
formData = collection.toMap();
|
|
imagePaths = _getCollectionImagePaths(collection);
|
|
await _localStorageService.saveAirSamplingRecord(_toMapForLocalSave(installationData!..collectionData = collection), refID, serverName: serverName);
|
|
}
|
|
|
|
final logData = {
|
|
'submission_id': refID,
|
|
'module': 'air',
|
|
'type': type,
|
|
'status': status,
|
|
'message': message,
|
|
'report_id': (data.airManId ?? installationData?.airManId)?.toString(),
|
|
'created_at': DateTime.now(),
|
|
'form_data': jsonEncode(formData),
|
|
'image_data': jsonEncode(imagePaths),
|
|
'server_name': serverName,
|
|
'api_status': jsonEncode(apiResults),
|
|
'ftp_status': jsonEncode(ftpStatuses),
|
|
};
|
|
await _dbHelper.saveSubmissionLog(logData);
|
|
}
|
|
|
|
// Helper and Alert methods remain unchanged
|
|
Future<void> _handleInstallationSuccessAlert(AirInstallationData data, List<Map<String, dynamic>>? appSettings, {required bool isDataOnly}) async {
|
|
try {
|
|
final message = data.generateInstallationTelegramAlert(isDataOnly: isDataOnly);
|
|
final bool wasSent = await _telegramService.sendAlertImmediately('air_manual', message, appSettings);
|
|
if (!wasSent) {
|
|
await _telegramService.queueMessage('air_manual', message, appSettings);
|
|
}
|
|
} catch (e) {
|
|
debugPrint("Failed to handle Air Manual Installation Telegram alert: $e");
|
|
}
|
|
}
|
|
|
|
Future<void> _handleCollectionSuccessAlert(AirCollectionData data, AirInstallationData installationData, List<Map<String, dynamic>>? appSettings, {required bool isDataOnly}) async {
|
|
try {
|
|
final message = data.generateCollectionTelegramAlert(installationData, isDataOnly: isDataOnly);
|
|
final bool wasSent = await _telegramService.sendAlertImmediately('air_manual', message, appSettings);
|
|
if (!wasSent) {
|
|
await _telegramService.queueMessage('air_manual', message, appSettings);
|
|
}
|
|
} catch (e) {
|
|
debugPrint("Failed to handle Air Manual Collection Telegram alert: $e");
|
|
}
|
|
}
|
|
|
|
List<String> _getInstallationImagePaths(AirInstallationData data) {
|
|
final List<File?> files = [
|
|
data.imageFront, data.imageBack, data.imageLeft, data.imageRight,
|
|
data.optionalImage1, data.optionalImage2, data.optionalImage3, data.optionalImage4,
|
|
];
|
|
return files.where((f) => f != null).map((f) => f!.path).toList();
|
|
}
|
|
|
|
List<String> _getCollectionImagePaths(AirCollectionData data) {
|
|
final List<File?> files = [
|
|
data.imageFront, data.imageBack, data.imageLeft, data.imageRight,
|
|
data.imageChart, data.imageFilterPaper,
|
|
data.optionalImage1, data.optionalImage2, data.optionalImage3, data.optionalImage4,
|
|
];
|
|
return files.where((f) => f != null).map((f) => f!.path).toList();
|
|
}
|
|
|
|
// getPendingInstallations can be moved to a different service or screen logic later
|
|
Future<List<AirInstallationData>> getPendingInstallations() async {
|
|
debugPrint("Fetching pending installations from local storage...");
|
|
final logs = await _dbHelper.loadSubmissionLogs(module: 'air');
|
|
final pendingInstallations = logs
|
|
?.where((log) {
|
|
final status = log['status'];
|
|
return status == 'S1' || status == 'S2';
|
|
})
|
|
.map((log) => AirInstallationData.fromJson(jsonDecode(log['form_data'])))
|
|
.toList() ?? [];
|
|
return pendingInstallations;
|
|
}
|
|
} |