- Introduced a new `logSafe` function for consistent logging with sensitivity handling. - Replaced direct logger calls with `logSafe` in `api_service.dart`, `app_initializer.dart`, `auth_service.dart`, `permission_service.dart`, and `my_image_compressor.dart`. - Enhanced error handling and logging in various service methods to capture exceptions and provide more context. - Updated image compression logging to include quality and size metrics. - Improved app initialization logging to capture success and error states. - Ensured sensitive information is not logged directly.
249 lines
8.6 KiB
Dart
249 lines
8.6 KiB
Dart
import 'package:file_picker/file_picker.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:marco/controller/my_controller.dart';
|
|
import 'package:marco/helpers/widgets/my_form_validator.dart';
|
|
import 'package:marco/helpers/services/api_service.dart';
|
|
import 'package:get/get.dart';
|
|
import 'package:marco/helpers/services/app_logger.dart';
|
|
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
|
import 'package:marco/controller/task_planing/daily_task_planing_controller.dart';
|
|
import 'package:image_picker/image_picker.dart';
|
|
import 'dart:io';
|
|
import 'dart:convert';
|
|
import 'package:marco/helpers/widgets/my_image_compressor.dart';
|
|
|
|
enum ApiStatus { idle, loading, success, failure }
|
|
|
|
final DailyTaskPlaningController taskController = Get.put(DailyTaskPlaningController());
|
|
final ImagePicker _picker = ImagePicker();
|
|
|
|
class ReportTaskController extends MyController {
|
|
List<PlatformFile> files = [];
|
|
MyFormValidator basicValidator = MyFormValidator();
|
|
RxBool isLoading = false.obs;
|
|
Rx<ApiStatus> reportStatus = ApiStatus.idle.obs;
|
|
Rx<ApiStatus> commentStatus = ApiStatus.idle.obs;
|
|
|
|
RxList<File> selectedImages = <File>[].obs;
|
|
|
|
final assignedDateController = TextEditingController();
|
|
final workAreaController = TextEditingController();
|
|
final activityController = TextEditingController();
|
|
final teamSizeController = TextEditingController();
|
|
final taskIdController = TextEditingController();
|
|
final assignedController = TextEditingController();
|
|
final completedWorkController = TextEditingController();
|
|
final commentController = TextEditingController();
|
|
final assignedByController = TextEditingController();
|
|
final teamMembersController = TextEditingController();
|
|
final plannedWorkController = TextEditingController();
|
|
|
|
@override
|
|
void onInit() {
|
|
super.onInit();
|
|
logSafe("Initializing ReportTaskController...");
|
|
basicValidator
|
|
..addField('assigned_date', label: "Assigned Date", controller: assignedDateController)
|
|
..addField('work_area', label: "Work Area", controller: workAreaController)
|
|
..addField('activity', label: "Activity", controller: activityController)
|
|
..addField('team_size', label: "Team Size", controller: teamSizeController)
|
|
..addField('task_id', label: "Task Id", controller: taskIdController)
|
|
..addField('assigned', label: "Assigned", controller: assignedController)
|
|
..addField('completed_work', label: "Completed Work", required: true, controller: completedWorkController)
|
|
..addField('comment', label: "Comment", required: true, controller: commentController)
|
|
..addField('assigned_by', label: "Assigned By", controller: assignedByController)
|
|
..addField('team_members', label: "Team Members", controller: teamMembersController)
|
|
..addField('planned_work', label: "Planned Work", controller: plannedWorkController);
|
|
logSafe("Form fields initialized.");
|
|
}
|
|
|
|
@override
|
|
void onClose() {
|
|
[
|
|
assignedDateController,
|
|
workAreaController,
|
|
activityController,
|
|
teamSizeController,
|
|
taskIdController,
|
|
assignedController,
|
|
completedWorkController,
|
|
commentController,
|
|
assignedByController,
|
|
teamMembersController,
|
|
plannedWorkController,
|
|
].forEach((controller) => controller.dispose());
|
|
super.onClose();
|
|
}
|
|
|
|
Future<bool> reportTask({
|
|
required String projectId,
|
|
required String comment,
|
|
required int completedTask,
|
|
required List<Map<String, dynamic>> checklist,
|
|
required DateTime reportedDate,
|
|
List<File>? images,
|
|
}) async {
|
|
logSafe("Reporting task for projectId", sensitive: true);
|
|
final completedWork = completedWorkController.text.trim();
|
|
if (completedWork.isEmpty || int.tryParse(completedWork) == null || int.parse(completedWork) < 0) {
|
|
_showError("Completed work must be a positive number.");
|
|
return false;
|
|
}
|
|
|
|
final commentField = commentController.text.trim();
|
|
if (commentField.isEmpty) {
|
|
_showError("Comment is required.");
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
reportStatus.value = ApiStatus.loading;
|
|
isLoading.value = true;
|
|
|
|
final imageData = await _prepareImages(images, "task report");
|
|
|
|
final success = await ApiService.reportTask(
|
|
id: projectId,
|
|
comment: commentField,
|
|
completedTask: int.parse(completedWork),
|
|
checkList: checklist,
|
|
images: imageData,
|
|
);
|
|
|
|
if (success) {
|
|
reportStatus.value = ApiStatus.success;
|
|
_showSuccess("Task reported successfully!");
|
|
await taskController.fetchTaskData(projectId);
|
|
return true;
|
|
} else {
|
|
reportStatus.value = ApiStatus.failure;
|
|
_showError("Failed to report task.");
|
|
return false;
|
|
}
|
|
} catch (e, s) {
|
|
logSafe("Exception while reporting task", level: LogLevel.error, error: e, stackTrace: s);
|
|
reportStatus.value = ApiStatus.failure;
|
|
_showError("An error occurred while reporting the task.");
|
|
return false;
|
|
} finally {
|
|
isLoading.value = false;
|
|
Future.delayed(const Duration(milliseconds: 500), () {
|
|
reportStatus.value = ApiStatus.idle;
|
|
});
|
|
}
|
|
}
|
|
|
|
Future<void> commentTask({
|
|
required String projectId,
|
|
required String comment,
|
|
List<File>? images,
|
|
}) async {
|
|
logSafe("Submitting comment for project", sensitive: true);
|
|
|
|
final commentField = commentController.text.trim();
|
|
if (commentField.isEmpty) {
|
|
_showError("Comment is required.");
|
|
return;
|
|
}
|
|
|
|
try {
|
|
isLoading.value = true;
|
|
final imageData = await _prepareImages(images, "task comment");
|
|
|
|
final success = await ApiService.commentTask(
|
|
id: projectId,
|
|
comment: commentField,
|
|
images: imageData,
|
|
).timeout(const Duration(seconds: 30), onTimeout: () {
|
|
logSafe("Task comment request timed out.", level: LogLevel.error);
|
|
throw Exception("Request timed out.");
|
|
});
|
|
|
|
if (success) {
|
|
_showSuccess("Task commented successfully!");
|
|
await taskController.fetchTaskData(projectId);
|
|
} else {
|
|
_showError("Failed to comment task.");
|
|
}
|
|
} catch (e, s) {
|
|
logSafe("Exception while commenting task", level: LogLevel.error, error: e, stackTrace: s);
|
|
_showError("An error occurred while commenting the task.");
|
|
} finally {
|
|
isLoading.value = false;
|
|
}
|
|
}
|
|
|
|
Future<List<Map<String, dynamic>>?> _prepareImages(List<File>? images, String context) async {
|
|
if (images == null || images.isEmpty) return null;
|
|
|
|
logSafe("Preparing images for $context upload...");
|
|
|
|
final results = await Future.wait(images.map((file) async {
|
|
try {
|
|
final compressed = await compressImageToUnder100KB(file);
|
|
if (compressed == null) return null;
|
|
|
|
return {
|
|
"fileName": file.path.split('/').last,
|
|
"base64Data": base64Encode(compressed),
|
|
"contentType": _getContentTypeFromFileName(file.path),
|
|
"fileSize": compressed.lengthInBytes,
|
|
"description": "Image uploaded for $context",
|
|
};
|
|
} catch (e) {
|
|
logSafe("Image processing failed: ${file.path}", level: LogLevel.warning, error: e);
|
|
return null;
|
|
}
|
|
}));
|
|
|
|
return results.whereType<Map<String, dynamic>>().toList();
|
|
}
|
|
|
|
String _getContentTypeFromFileName(String fileName) {
|
|
final ext = fileName.split('.').last.toLowerCase();
|
|
return switch (ext) {
|
|
'jpg' || 'jpeg' => 'image/jpeg',
|
|
'png' => 'image/png',
|
|
'webp' => 'image/webp',
|
|
'gif' => 'image/gif',
|
|
_ => 'application/octet-stream',
|
|
};
|
|
}
|
|
|
|
Future<void> pickImages({required bool fromCamera}) async {
|
|
try {
|
|
if (fromCamera) {
|
|
final pickedFile = await _picker.pickImage(source: ImageSource.camera, imageQuality: 75);
|
|
if (pickedFile != null) {
|
|
selectedImages.add(File(pickedFile.path));
|
|
}
|
|
} else {
|
|
final pickedFiles = await _picker.pickMultiImage(imageQuality: 75);
|
|
selectedImages.addAll(pickedFiles.map((xfile) => File(xfile.path)));
|
|
}
|
|
logSafe("Images picked: ${selectedImages.length}", sensitive: true);
|
|
} catch (e) {
|
|
logSafe("Error picking images", level: LogLevel.warning, error: e);
|
|
}
|
|
}
|
|
|
|
void removeImageAt(int index) {
|
|
if (index >= 0 && index < selectedImages.length) {
|
|
selectedImages.removeAt(index);
|
|
logSafe("Removed image at index $index");
|
|
}
|
|
}
|
|
|
|
void _showError(String message) => showAppSnackbar(
|
|
title: "Error",
|
|
message: message,
|
|
type: SnackbarType.error,
|
|
);
|
|
|
|
void _showSuccess(String message) => showAppSnackbar(
|
|
title: "Success",
|
|
message: message,
|
|
type: SnackbarType.success,
|
|
);
|
|
}
|