171 lines
6.3 KiB
Dart
171 lines
6.3 KiB
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';
|
|
|
|
/// A dedicated service to handle all business logic for the Air Manual Sampling feature.
|
|
class AirSamplingService {
|
|
final ApiService _apiService = ApiService();
|
|
final LocalStorageService _localStorageService = LocalStorageService();
|
|
|
|
/// 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,
|
|
}) 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;
|
|
|
|
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}_INSTALL_${imageInfo.replaceAll(' ', '')}.jpg";
|
|
final filePath = path.join(tempDir.path, newFileName);
|
|
|
|
return File(filePath)..writeAsBytesSync(img.encodeJpg(originalImage));
|
|
}
|
|
|
|
/// Orchestrates a two-step submission process for air installation samples.
|
|
Future<Map<String, dynamic>> submitInstallation(AirInstallationData data) async {
|
|
// --- OFFLINE-FIRST HELPER ---
|
|
Future<Map<String, dynamic>> saveLocally() async {
|
|
debugPrint("Saving installation locally...");
|
|
data.status = 'L1'; // Mark as Locally Saved, Pending Submission
|
|
await _localStorageService.saveAirSamplingRecord(data.toMap(), data.refID!);
|
|
return {
|
|
'status': 'L1',
|
|
'message': 'No connection or server error. Installation data saved locally.',
|
|
};
|
|
}
|
|
|
|
// --- 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();
|
|
}
|
|
|
|
final recordId = textDataResult['data']?['air_man_id'];
|
|
if (recordId == null) {
|
|
debugPrint("Text data submitted, but did not receive a record ID.");
|
|
return await saveLocally();
|
|
}
|
|
debugPrint("Text data submitted successfully. Received record ID: $recordId");
|
|
|
|
// --- STEP 2: UPLOAD IMAGE FILES ---
|
|
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!);
|
|
return {'status': 'S1', 'message': 'Installation data submitted successfully.'};
|
|
}
|
|
|
|
debugPrint("Step 2: Uploading ${filesToUpload.length} images for record ID $recordId...");
|
|
final imageUploadResult = await _apiService.air.uploadInstallationImages(
|
|
airManId: recordId.toString(),
|
|
files: filesToUpload,
|
|
);
|
|
|
|
if (imageUploadResult['success'] != true) {
|
|
debugPrint("Image upload failed. Reason: ${imageUploadResult['message']}");
|
|
return await saveLocally();
|
|
}
|
|
|
|
debugPrint("Images uploaded successfully.");
|
|
data.status = 'S2'; // Server Pending (images uploaded)
|
|
await _localStorageService.saveAirSamplingRecord(data.toMap(), data.refID!);
|
|
return {
|
|
'status': 'S2',
|
|
'message': 'Installation data and images submitted successfully.',
|
|
};
|
|
}
|
|
|
|
|
|
/// Submits only the collection data, linked to a previous installation.
|
|
Future<Map<String, dynamic>> submitCollection(AirCollectionData data) async {
|
|
try {
|
|
final result = await _apiService.post(
|
|
'air/manual/collection',
|
|
data.toJson()
|
|
);
|
|
|
|
if (result['success'] == true) {
|
|
data.status = 'S3'; // Server Completed
|
|
// Also save the completed record locally
|
|
await _localStorageService.saveAirSamplingRecord(data.toMap(), data.installationRefID!);
|
|
return {'status': 'S3', 'message': 'Collection submitted successfully.'};
|
|
} else {
|
|
throw Exception(result['message'] ?? 'Unknown server error');
|
|
}
|
|
} catch (e) {
|
|
debugPrint("API submission failed: $e. Saving collection locally.");
|
|
data.status = 'L3'; // Mark as completed locally
|
|
|
|
// CORRECTED: Use toMap() to ensure all data is saved locally on failure.
|
|
await _localStorageService.saveAirSamplingRecord(data.toMap(), data.installationRefID!);
|
|
|
|
return {
|
|
'status': 'L3',
|
|
'message': 'No connection. Collection data saved locally.',
|
|
};
|
|
}
|
|
}
|
|
|
|
/// 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: Include 'S2' to show records that have successfully uploaded images
|
|
// but are still pending collection.
|
|
return status == 'L1' || status == 'S1' || status == 'S2';
|
|
})
|
|
.map((log) => AirInstallationData.fromJson(log))
|
|
.toList();
|
|
|
|
return pendingInstallations;
|
|
}
|
|
|
|
void dispose() {
|
|
// Clean up any resources if necessary
|
|
}
|
|
}
|