environment_monitoring_app/lib/services/air_sampling_service.dart

316 lines
15 KiB
Dart

// lib/services/air_sampling_service.dart
import 'dart:async';
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 '../models/air_installation_data.dart';
import '../models/air_collection_data.dart';
import 'api_service.dart';
import 'local_storage_service.dart';
import 'telegram_service.dart';
// --- ADDED: Import for the service that manages active server configurations ---
import 'server_config_service.dart';
/// A dedicated service to handle all business logic for the Air Manual Sampling feature.
class AirSamplingService {
final ApiService _apiService = ApiService();
final LocalStorageService _localStorageService = LocalStorageService();
final TelegramService _telegramService = TelegramService();
// --- ADDED: An instance of the service to get the active server name ---
final ServerConfigService _serverConfigService = ServerConfigService();
/// Picks an image from the specified source, adds a timestamp watermark,
/// and saves it to a temporary directory with a standardized name.
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;
// MODIFIED: Enforce landscape orientation for required photos
if (isRequired && originalImage.height > originalImage.width) {
debugPrint("Image orientation check failed: Image must be in landscape mode.");
return null; // Return null to indicate failure
}
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));
}
// MODIFIED: Method now requires the appSettings list to pass to TelegramService.
Future<void> _handleInstallationSuccessAlert(AirInstallationData data, List<Map<String, dynamic>>? appSettings, {required bool isDataOnly}) async {
try {
final message = data.generateInstallationTelegramAlert(isDataOnly: isDataOnly);
// Pass the appSettings list to the telegram service methods
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");
}
}
// MODIFIED: Method now requires the appSettings list to pass to TelegramService.
Future<void> _handleCollectionSuccessAlert(AirCollectionData data, AirInstallationData installationData, List<Map<String, dynamic>>? appSettings, {required bool isDataOnly}) async {
try {
final message = data.generateCollectionTelegramAlert(installationData, isDataOnly: isDataOnly);
// Pass the appSettings list to the telegram service methods
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");
}
}
/// Orchestrates a two-step submission process for air installation samples.
// MODIFIED: Method now requires the appSettings list to pass down the call stack.
Future<Map<String, dynamic>> submitInstallation(AirInstallationData data, List<Map<String, dynamic>>? appSettings) async {
// --- MODIFIED: Get the active server name to use for local storage ---
final activeConfig = await _serverConfigService.getActiveApiConfig();
final serverName = activeConfig?['config_name'] as String? ?? 'Default';
// --- OFFLINE-FIRST HELPER ---
Future<Map<String, dynamic>> saveLocally(String status, String message) async {
debugPrint("Saving installation locally with status: $status");
data.status = status; // Use the provided status
// --- MODIFIED: Pass the serverName to the save method ---
await _localStorageService.saveAirSamplingRecord(data.toMap(), data.refID!, serverName: serverName);
return {'status': status, 'message': message};
}
// If the record's text data is already on the server, skip directly to image upload.
if (data.status == 'L2_PENDING_IMAGES' && data.airManId != null) {
debugPrint("Retrying image upload for existing record ID: ${data.airManId}");
return await _uploadInstallationImagesAndUpdate(data, appSettings, serverName: serverName);
}
// --- STEP 1: SUBMIT TEXT DATA ---
debugPrint("Step 1: Submitting installation text data...");
final textDataResult = await _apiService.post('air/manual/installation', data.toJsonForApi());
if (textDataResult['success'] != true) {
debugPrint("Failed to submit text data. Reason: ${textDataResult['message']}");
return await saveLocally('L1', 'No connection or server error. Installation data saved locally.');
}
// --- NECESSARY FIX: Safely parse the record ID from the server response ---
final dynamic recordIdFromServer = textDataResult['data']?['air_man_id'];
if (recordIdFromServer == null) {
debugPrint("Text data submitted, but did not receive a record ID.");
return await saveLocally('L1', 'Data submitted, but server response was invalid.');
}
debugPrint("Text data submitted successfully. Received record ID: $recordIdFromServer");
final int? parsedRecordId = int.tryParse(recordIdFromServer.toString());
if (parsedRecordId == null) {
debugPrint("Could not parse the received record ID: $recordIdFromServer");
return await saveLocally('L1', 'Data submitted, but server response was invalid.');
}
data.airManId = parsedRecordId;
// --- STEP 2: UPLOAD IMAGE FILES ---
return await _uploadInstallationImagesAndUpdate(data, appSettings, serverName: serverName);
}
/// A reusable function for handling the image upload and local data update logic.
// MODIFIED: Method now requires the serverName to pass to the save method.
Future<Map<String, dynamic>> _uploadInstallationImagesAndUpdate(AirInstallationData data, List<Map<String, dynamic>>? appSettings, {required String serverName}) async {
final filesToUpload = data.getImagesForUpload();
if (filesToUpload.isEmpty) {
debugPrint("No images to upload. Submission complete.");
data.status = 'S1'; // Server Pending (no images needed)
await _localStorageService.saveAirSamplingRecord(data.toMap(), data.refID!, serverName: serverName);
_handleInstallationSuccessAlert(data, appSettings, isDataOnly: true);
return {'status': 'S1', 'message': 'Installation data submitted successfully.'};
}
debugPrint("Step 2: Uploading ${filesToUpload.length} images for record ID ${data.airManId}...");
final imageUploadResult = await _apiService.air.uploadInstallationImages(
airManId: data.airManId.toString(),
files: filesToUpload,
);
if (imageUploadResult['success'] != true) {
debugPrint("Image upload failed. Reason: ${imageUploadResult['message']}");
data.status = 'L2_PENDING_IMAGES';
await _localStorageService.saveAirSamplingRecord(data.toMap(), data.refID!, serverName: serverName);
return {
'status': 'L2_PENDING_IMAGES',
'message': 'Data submitted, but image upload failed. Saved locally for retry.',
};
}
debugPrint("Images uploaded successfully.");
data.status = 'S2'; // Server Pending (images uploaded)
await _localStorageService.saveAirSamplingRecord(data.toMap(), data.refID!, serverName: serverName);
_handleInstallationSuccessAlert(data, appSettings, isDataOnly: false);
return {
'status': 'S2',
'message': 'Installation data and images submitted successfully.',
};
}
/// Submits only the collection data, linked to a previous installation.
// MODIFIED: Method now requires the appSettings list to pass down the call stack.
Future<Map<String, dynamic>> submitCollection(AirCollectionData data, AirInstallationData installationData, List<Map<String, dynamic>>? appSettings) async {
// --- MODIFIED: Get the active server name to use for local storage ---
final activeConfig = await _serverConfigService.getActiveApiConfig();
final serverName = activeConfig?['config_name'] as String? ?? 'Default';
// --- OFFLINE-FIRST HELPER (CORRECTED) ---
Future<Map<String, dynamic>> updateAndSaveLocally(String newStatus, {String? message}) async {
debugPrint("Saving collection data locally with status: $newStatus");
final allLogs = await _localStorageService.getAllAirSamplingLogs();
final logIndex = allLogs.indexWhere((log) => log['refID'] == data.installationRefID);
if (logIndex != -1) {
final installationLog = allLogs[logIndex];
// FIX: Nest collection data to prevent overwriting installation fields.
installationLog['collectionData'] = data.toMap();
installationLog['status'] = newStatus; // Update the overall status
await _localStorageService.saveAirSamplingRecord(installationLog, data.installationRefID!, serverName: serverName);
}
return {
'status': newStatus,
'message': message ?? 'No connection or server error. Data saved locally.',
};
}
// If the record's text data is already on the server, skip directly to image upload.
if (data.status == 'L4_PENDING_IMAGES' && data.airManId != null) {
debugPrint("Retrying collection image upload for existing record ID: ${data.airManId}");
return await _uploadCollectionImagesAndUpdate(data, installationData, appSettings, serverName: serverName);
}
// --- STEP 1: SUBMIT TEXT DATA ---
debugPrint("Step 1: Submitting collection text data...");
final textDataResult = await _apiService.post('air/manual/collection', data.toJson());
if (textDataResult['success'] != true) {
debugPrint("Failed to submit collection text data. Reason: ${textDataResult['message']}");
return await updateAndSaveLocally('L3', message: 'No connection or server error. Collection data saved locally.');
}
debugPrint("Collection text data submitted successfully.");
// --- STEP 2: UPLOAD IMAGE FILES ---
return await _uploadCollectionImagesAndUpdate(data, installationData, appSettings, serverName: serverName);
}
/// A reusable function for handling the collection image upload and local data update logic.
// MODIFIED: Method now requires the serverName to pass to the save method.
Future<Map<String, dynamic>> _uploadCollectionImagesAndUpdate(AirCollectionData data, AirInstallationData installationData, List<Map<String, dynamic>>? appSettings, {required String serverName}) async {
// --- OFFLINE-FIRST HELPER (CORRECTED & MODIFIED) ---
Future<Map<String, dynamic>> updateAndSaveLocally(String newStatus, {String? message}) async {
debugPrint("Saving collection data locally with status: $newStatus");
final allLogs = await _localStorageService.getAllAirSamplingLogs();
final logIndex = allLogs.indexWhere((log) => log['refID'] == data.installationRefID);
if (logIndex != -1) {
final installationLog = allLogs[logIndex];
installationLog['collectionData'] = data.toMap();
installationLog['status'] = newStatus;
await _localStorageService.saveAirSamplingRecord(installationLog, data.installationRefID!, serverName: serverName);
}
return {
'status': newStatus,
'message': message ?? 'No connection or server error. Data saved locally.',
};
}
final filesToUpload = data.getImagesForUpload();
if (filesToUpload.isEmpty) {
debugPrint("No collection images to upload. Submission complete.");
await updateAndSaveLocally('S3'); // S3 = Server Completed
_handleCollectionSuccessAlert(data, installationData, appSettings, isDataOnly: true);
return {'status': 'S3', 'message': 'Collection data submitted successfully.'};
}
debugPrint("Step 2: Uploading ${filesToUpload.length} collection images...");
final imageUploadResult = await _apiService.air.uploadCollectionImages(
airManId: data.airManId.toString(),
files: filesToUpload,
);
if (imageUploadResult['success'] != true) {
debugPrint("Image upload failed. Reason: ${imageUploadResult['message']}");
// Use status 'L4_PENDING_IMAGES' to indicate text submitted but images failed
return await updateAndSaveLocally('L4_PENDING_IMAGES', message: 'Data submitted, but image upload failed. Saved locally for retry.');
}
debugPrint("Images uploaded successfully.");
await updateAndSaveLocally('S3'); // S3 = Server Completed
_handleCollectionSuccessAlert(data, installationData, appSettings, isDataOnly: false);
return {
'status': 'S3',
'message': 'Collection data and images submitted successfully.',
};
}
/// Fetches installations that are pending collection from local storage.
Future<List<AirInstallationData>> getPendingInstallations() async {
debugPrint("Fetching pending installations from local storage...");
final logs = await _localStorageService.getAllAirSamplingLogs();
final pendingInstallations = logs
.where((log) {
final status = log['status'];
// --- CORRECTED ---
// Only show installations that have been synced to the server (S1, S2).
// 'L1' (Local only) records cannot be collected until they are synced.
return status == 'S1' || status == 'S2';
})
.map((log) => AirInstallationData.fromJson(log))
.toList();
return pendingInstallations;
}
void dispose() {
// Clean up any resources if necessary
}
}