feat: Add timestamp functionality to images in attendance and expense controllers
- Implemented TimestampImageHelper to add timestamps to images. - Updated AttendanceController to apply timestamps when capturing images. - Enhanced AddExpenseController to process images with timestamps. - Modified ReportTaskActionController and ReportTaskController to include timestamping for images. - Updated UI components to show loading indicators while processing images. - Refactored image picking logic to handle timestamping and loading states.
This commit is contained in:
parent
16a2e1e53a
commit
d208648350
@ -8,6 +8,7 @@ import 'package:intl/intl.dart';
|
|||||||
import 'package:marco/helpers/services/app_logger.dart';
|
import 'package:marco/helpers/services/app_logger.dart';
|
||||||
import 'package:marco/helpers/services/api_service.dart';
|
import 'package:marco/helpers/services/api_service.dart';
|
||||||
import 'package:marco/helpers/widgets/my_image_compressor.dart';
|
import 'package:marco/helpers/widgets/my_image_compressor.dart';
|
||||||
|
import 'package:marco/helpers/widgets/time_stamp_image_helper.dart';
|
||||||
|
|
||||||
import 'package:marco/model/attendance/attendance_model.dart';
|
import 'package:marco/model/attendance/attendance_model.dart';
|
||||||
import 'package:marco/model/project_model.dart';
|
import 'package:marco/model/project_model.dart';
|
||||||
@ -32,7 +33,7 @@ class AttendanceController extends GetxController {
|
|||||||
final isLoadingOrganizations = false.obs;
|
final isLoadingOrganizations = false.obs;
|
||||||
|
|
||||||
// States
|
// States
|
||||||
String selectedTab = 'todaysAttendance';
|
String selectedTab = 'todaysAttendance';
|
||||||
DateTime? startDateAttendance;
|
DateTime? startDateAttendance;
|
||||||
DateTime? endDateAttendance;
|
DateTime? endDateAttendance;
|
||||||
|
|
||||||
@ -104,7 +105,7 @@ String selectedTab = 'todaysAttendance';
|
|||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Computed filtered regularization logs
|
// Computed filtered regularization logs
|
||||||
List<RegularizationLogModel> get filteredRegularizationLogs {
|
List<RegularizationLogModel> get filteredRegularizationLogs {
|
||||||
if (searchQuery.value.isEmpty) return regularizationLogs;
|
if (searchQuery.value.isEmpty) return regularizationLogs;
|
||||||
return regularizationLogs
|
return regularizationLogs
|
||||||
@ -174,8 +175,12 @@ String selectedTab = 'todaysAttendance';
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 🔹 Add timestamp to the image
|
||||||
|
final timestampedFile = await TimestampImageHelper.addTimestamp(
|
||||||
|
imageFile: File(image.path));
|
||||||
|
|
||||||
final compressedBytes =
|
final compressedBytes =
|
||||||
await compressImageToUnder100KB(File(image.path));
|
await compressImageToUnder100KB(timestampedFile);
|
||||||
if (compressedBytes == null) {
|
if (compressedBytes == null) {
|
||||||
logSafe("Image compression failed.", level: LogLevel.error);
|
logSafe("Image compression failed.", level: LogLevel.error);
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@ -17,6 +17,7 @@ import 'package:marco/helpers/widgets/my_snackbar.dart';
|
|||||||
import 'package:marco/model/employees/employee_model.dart';
|
import 'package:marco/model/employees/employee_model.dart';
|
||||||
import 'package:marco/model/expense/expense_type_model.dart';
|
import 'package:marco/model/expense/expense_type_model.dart';
|
||||||
import 'package:marco/model/expense/payment_types_model.dart';
|
import 'package:marco/model/expense/payment_types_model.dart';
|
||||||
|
import 'package:marco/helpers/widgets/time_stamp_image_helper.dart';
|
||||||
|
|
||||||
class AddExpenseController extends GetxController {
|
class AddExpenseController extends GetxController {
|
||||||
// --- Text Controllers ---
|
// --- Text Controllers ---
|
||||||
@ -65,6 +66,7 @@ class AddExpenseController extends GetxController {
|
|||||||
final paymentModes = <PaymentModeModel>[].obs;
|
final paymentModes = <PaymentModeModel>[].obs;
|
||||||
final allEmployees = <EmployeeModel>[].obs;
|
final allEmployees = <EmployeeModel>[].obs;
|
||||||
final employeeSearchResults = <EmployeeModel>[].obs;
|
final employeeSearchResults = <EmployeeModel>[].obs;
|
||||||
|
final isProcessingAttachment = false.obs;
|
||||||
|
|
||||||
String? editingExpenseId;
|
String? editingExpenseId;
|
||||||
|
|
||||||
@ -252,9 +254,22 @@ class AddExpenseController extends GetxController {
|
|||||||
Future<void> pickFromCamera() async {
|
Future<void> pickFromCamera() async {
|
||||||
try {
|
try {
|
||||||
final pickedFile = await _picker.pickImage(source: ImageSource.camera);
|
final pickedFile = await _picker.pickImage(source: ImageSource.camera);
|
||||||
if (pickedFile != null) attachments.add(File(pickedFile.path));
|
if (pickedFile != null) {
|
||||||
|
isProcessingAttachment.value = true; // start loading
|
||||||
|
File imageFile = File(pickedFile.path);
|
||||||
|
|
||||||
|
// Add timestamp to the captured image
|
||||||
|
File timestampedFile = await TimestampImageHelper.addTimestamp(
|
||||||
|
imageFile: imageFile,
|
||||||
|
);
|
||||||
|
|
||||||
|
attachments.add(timestampedFile);
|
||||||
|
attachments.refresh(); // refresh UI
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
_errorSnackbar("Camera error: $e");
|
_errorSnackbar("Camera error: $e");
|
||||||
|
} finally {
|
||||||
|
isProcessingAttachment.value = false; // stop loading
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -13,6 +13,7 @@ import 'package:marco/helpers/widgets/my_form_validator.dart';
|
|||||||
import 'package:marco/helpers/widgets/my_image_compressor.dart';
|
import 'package:marco/helpers/widgets/my_image_compressor.dart';
|
||||||
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
||||||
import 'package:marco/model/dailyTaskPlanning/work_status_model.dart';
|
import 'package:marco/model/dailyTaskPlanning/work_status_model.dart';
|
||||||
|
import 'package:marco/helpers/widgets/time_stamp_image_helper.dart';
|
||||||
|
|
||||||
enum ApiStatus { idle, loading, success, failure }
|
enum ApiStatus { idle, loading, success, failure }
|
||||||
|
|
||||||
@ -32,9 +33,11 @@ class ReportTaskActionController extends MyController {
|
|||||||
final Rxn<Map<String, dynamic>> selectedTask = Rxn<Map<String, dynamic>>();
|
final Rxn<Map<String, dynamic>> selectedTask = Rxn<Map<String, dynamic>>();
|
||||||
|
|
||||||
final RxString selectedWorkStatusName = ''.obs;
|
final RxString selectedWorkStatusName = ''.obs;
|
||||||
|
final RxBool isPickingImage = false.obs;
|
||||||
|
|
||||||
final MyFormValidator basicValidator = MyFormValidator();
|
final MyFormValidator basicValidator = MyFormValidator();
|
||||||
final DailyTaskPlanningController taskController = Get.put(DailyTaskPlanningController());
|
final DailyTaskPlanningController taskController =
|
||||||
|
Get.put(DailyTaskPlanningController());
|
||||||
final ImagePicker _picker = ImagePicker();
|
final ImagePicker _picker = ImagePicker();
|
||||||
|
|
||||||
final assignedDateController = TextEditingController();
|
final assignedDateController = TextEditingController();
|
||||||
@ -83,18 +86,31 @@ class ReportTaskActionController extends MyController {
|
|||||||
|
|
||||||
void _initializeFormFields() {
|
void _initializeFormFields() {
|
||||||
basicValidator
|
basicValidator
|
||||||
..addField('assigned_date', label: "Assigned Date", controller: assignedDateController)
|
..addField('assigned_date',
|
||||||
..addField('work_area', label: "Work Area", controller: workAreaController)
|
label: "Assigned Date", controller: assignedDateController)
|
||||||
|
..addField('work_area',
|
||||||
|
label: "Work Area", controller: workAreaController)
|
||||||
..addField('activity', label: "Activity", controller: activityController)
|
..addField('activity', label: "Activity", controller: activityController)
|
||||||
..addField('team_size', label: "Team Size", controller: teamSizeController)
|
..addField('team_size',
|
||||||
|
label: "Team Size", controller: teamSizeController)
|
||||||
..addField('task_id', label: "Task Id", controller: taskIdController)
|
..addField('task_id', label: "Task Id", controller: taskIdController)
|
||||||
..addField('assigned', label: "Assigned", controller: assignedController)
|
..addField('assigned', label: "Assigned", controller: assignedController)
|
||||||
..addField('completed_work', label: "Completed Work", required: true, controller: completedWorkController)
|
..addField('completed_work',
|
||||||
..addField('comment', label: "Comment", required: true, controller: commentController)
|
label: "Completed Work",
|
||||||
..addField('assigned_by', label: "Assigned By", controller: assignedByController)
|
required: true,
|
||||||
..addField('team_members', label: "Team Members", controller: teamMembersController)
|
controller: completedWorkController)
|
||||||
..addField('planned_work', label: "Planned Work", controller: plannedWorkController)
|
..addField('comment',
|
||||||
..addField('approved_task', label: "Approved Task", required: true, controller: approvedTaskController);
|
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)
|
||||||
|
..addField('approved_task',
|
||||||
|
label: "Approved Task",
|
||||||
|
required: true,
|
||||||
|
controller: approvedTaskController);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> approveTask({
|
Future<bool> approveTask({
|
||||||
@ -108,7 +124,8 @@ class ReportTaskActionController extends MyController {
|
|||||||
|
|
||||||
if (projectId.isEmpty || reportActionId.isEmpty) {
|
if (projectId.isEmpty || reportActionId.isEmpty) {
|
||||||
_showError("Project ID and Report Action ID are required.");
|
_showError("Project ID and Report Action ID are required.");
|
||||||
logSafe("Missing required projectId or reportActionId", level: LogLevel.warning);
|
logSafe("Missing required projectId or reportActionId",
|
||||||
|
level: LogLevel.warning);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -117,13 +134,15 @@ class ReportTaskActionController extends MyController {
|
|||||||
|
|
||||||
if (approvedTaskInt == null) {
|
if (approvedTaskInt == null) {
|
||||||
_showError("Invalid approved task count.");
|
_showError("Invalid approved task count.");
|
||||||
logSafe("Invalid approvedTaskCount: $approvedTaskCount", level: LogLevel.warning);
|
logSafe("Invalid approvedTaskCount: $approvedTaskCount",
|
||||||
|
level: LogLevel.warning);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (completedWorkInt != null && approvedTaskInt > completedWorkInt) {
|
if (completedWorkInt != null && approvedTaskInt > completedWorkInt) {
|
||||||
_showError("Approved task count cannot exceed completed work.");
|
_showError("Approved task count cannot exceed completed work.");
|
||||||
logSafe("Validation failed: approved > completed", level: LogLevel.warning);
|
logSafe("Validation failed: approved > completed",
|
||||||
|
level: LogLevel.warning);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -159,7 +178,8 @@ class ReportTaskActionController extends MyController {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} catch (e, st) {
|
} catch (e, st) {
|
||||||
logSafe("Error in approveTask: $e", level: LogLevel.error, error: e, stackTrace: st);
|
logSafe("Error in approveTask: $e",
|
||||||
|
level: LogLevel.error, error: e, stackTrace: st);
|
||||||
_showError("An error occurred.");
|
_showError("An error occurred.");
|
||||||
return false;
|
return false;
|
||||||
} finally {
|
} finally {
|
||||||
@ -207,7 +227,8 @@ class ReportTaskActionController extends MyController {
|
|||||||
_showError("Failed to comment task.");
|
_showError("Failed to comment task.");
|
||||||
}
|
}
|
||||||
} catch (e, st) {
|
} catch (e, st) {
|
||||||
logSafe("Error in commentTask: $e", level: LogLevel.error, error: e, stackTrace: st);
|
logSafe("Error in commentTask: $e",
|
||||||
|
level: LogLevel.error, error: e, stackTrace: st);
|
||||||
_showError("An error occurred while commenting the task.");
|
_showError("An error occurred while commenting the task.");
|
||||||
} finally {
|
} finally {
|
||||||
isLoading.value = false;
|
isLoading.value = false;
|
||||||
@ -224,7 +245,8 @@ class ReportTaskActionController extends MyController {
|
|||||||
workStatus.assignAll(model.data);
|
workStatus.assignAll(model.data);
|
||||||
logSafe("Fetched ${model.data.length} work statuses");
|
logSafe("Fetched ${model.data.length} work statuses");
|
||||||
} else {
|
} else {
|
||||||
logSafe("No work statuses found or API call failed", level: LogLevel.warning);
|
logSafe("No work statuses found or API call failed",
|
||||||
|
level: LogLevel.warning);
|
||||||
}
|
}
|
||||||
|
|
||||||
isLoadingWorkStatus.value = false;
|
isLoadingWorkStatus.value = false;
|
||||||
@ -251,7 +273,8 @@ class ReportTaskActionController extends MyController {
|
|||||||
};
|
};
|
||||||
}));
|
}));
|
||||||
|
|
||||||
logSafe("_prepareImages: Prepared ${results.whereType<Map<String, dynamic>>().length} images.");
|
logSafe(
|
||||||
|
"_prepareImages: Prepared ${results.whereType<Map<String, dynamic>>().length} images.");
|
||||||
return results.whereType<Map<String, dynamic>>().toList();
|
return results.whereType<Map<String, dynamic>>().toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -267,23 +290,40 @@ class ReportTaskActionController extends MyController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> pickImages({required bool fromCamera}) async {
|
Future<void> pickImages({required bool fromCamera}) async {
|
||||||
logSafe("Opening image picker...");
|
try {
|
||||||
if (fromCamera) {
|
isPickingImage.value = true; // start loading
|
||||||
final pickedFile = await _picker.pickImage(source: ImageSource.camera, imageQuality: 75);
|
logSafe("Opening image picker...");
|
||||||
if (pickedFile != null) {
|
|
||||||
selectedImages.add(File(pickedFile.path));
|
if (fromCamera) {
|
||||||
logSafe("Image added from camera: ${pickedFile.path}", );
|
final pickedFile = await _picker.pickImage(
|
||||||
|
source: ImageSource.camera,
|
||||||
|
imageQuality: 75,
|
||||||
|
);
|
||||||
|
if (pickedFile != null) {
|
||||||
|
final timestampedFile = await TimestampImageHelper.addTimestamp(
|
||||||
|
imageFile: File(pickedFile.path),
|
||||||
|
);
|
||||||
|
selectedImages.add(timestampedFile);
|
||||||
|
logSafe("Image added from camera with timestamp: ${pickedFile.path}");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
final pickedFiles = await _picker.pickMultiImage(imageQuality: 75);
|
||||||
|
selectedImages.addAll(pickedFiles.map((xfile) => File(xfile.path)));
|
||||||
|
logSafe("${pickedFiles.length} images added from gallery.");
|
||||||
}
|
}
|
||||||
} else {
|
} catch (e, st) {
|
||||||
final pickedFiles = await _picker.pickMultiImage(imageQuality: 75);
|
logSafe("Error picking images: $e",
|
||||||
selectedImages.addAll(pickedFiles.map((xfile) => File(xfile.path)));
|
level: LogLevel.error, stackTrace: st);
|
||||||
logSafe("${pickedFiles.length} images added from gallery.", );
|
} finally {
|
||||||
|
isPickingImage.value = false; // stop loading
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void removeImageAt(int index) {
|
void removeImageAt(int index) {
|
||||||
if (index >= 0 && index < selectedImages.length) {
|
if (index >= 0 && index < selectedImages.length) {
|
||||||
logSafe("Removing image at index $index", );
|
logSafe(
|
||||||
|
"Removing image at index $index",
|
||||||
|
);
|
||||||
selectedImages.removeAt(index);
|
selectedImages.removeAt(index);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,10 +11,12 @@ import 'package:image_picker/image_picker.dart';
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'package:marco/helpers/widgets/my_image_compressor.dart';
|
import 'package:marco/helpers/widgets/my_image_compressor.dart';
|
||||||
|
import 'package:marco/helpers/widgets/time_stamp_image_helper.dart';
|
||||||
|
|
||||||
enum ApiStatus { idle, loading, success, failure }
|
enum ApiStatus { idle, loading, success, failure }
|
||||||
|
|
||||||
final DailyTaskPlanningController taskController = Get.put(DailyTaskPlanningController());
|
final DailyTaskPlanningController taskController =
|
||||||
|
Get.put(DailyTaskPlanningController());
|
||||||
final ImagePicker _picker = ImagePicker();
|
final ImagePicker _picker = ImagePicker();
|
||||||
|
|
||||||
class ReportTaskController extends MyController {
|
class ReportTaskController extends MyController {
|
||||||
@ -23,6 +25,7 @@ class ReportTaskController extends MyController {
|
|||||||
RxBool isLoading = false.obs;
|
RxBool isLoading = false.obs;
|
||||||
Rx<ApiStatus> reportStatus = ApiStatus.idle.obs;
|
Rx<ApiStatus> reportStatus = ApiStatus.idle.obs;
|
||||||
Rx<ApiStatus> commentStatus = ApiStatus.idle.obs;
|
Rx<ApiStatus> commentStatus = ApiStatus.idle.obs;
|
||||||
|
final RxBool isPickingImage = false.obs;
|
||||||
|
|
||||||
RxList<File> selectedImages = <File>[].obs;
|
RxList<File> selectedImages = <File>[].obs;
|
||||||
|
|
||||||
@ -43,17 +46,27 @@ class ReportTaskController extends MyController {
|
|||||||
super.onInit();
|
super.onInit();
|
||||||
logSafe("Initializing ReportTaskController...");
|
logSafe("Initializing ReportTaskController...");
|
||||||
basicValidator
|
basicValidator
|
||||||
..addField('assigned_date', label: "Assigned Date", controller: assignedDateController)
|
..addField('assigned_date',
|
||||||
..addField('work_area', label: "Work Area", controller: workAreaController)
|
label: "Assigned Date", controller: assignedDateController)
|
||||||
|
..addField('work_area',
|
||||||
|
label: "Work Area", controller: workAreaController)
|
||||||
..addField('activity', label: "Activity", controller: activityController)
|
..addField('activity', label: "Activity", controller: activityController)
|
||||||
..addField('team_size', label: "Team Size", controller: teamSizeController)
|
..addField('team_size',
|
||||||
|
label: "Team Size", controller: teamSizeController)
|
||||||
..addField('task_id', label: "Task Id", controller: taskIdController)
|
..addField('task_id', label: "Task Id", controller: taskIdController)
|
||||||
..addField('assigned', label: "Assigned", controller: assignedController)
|
..addField('assigned', label: "Assigned", controller: assignedController)
|
||||||
..addField('completed_work', label: "Completed Work", required: true, controller: completedWorkController)
|
..addField('completed_work',
|
||||||
..addField('comment', label: "Comment", required: true, controller: commentController)
|
label: "Completed Work",
|
||||||
..addField('assigned_by', label: "Assigned By", controller: assignedByController)
|
required: true,
|
||||||
..addField('team_members', label: "Team Members", controller: teamMembersController)
|
controller: completedWorkController)
|
||||||
..addField('planned_work', label: "Planned Work", controller: plannedWorkController);
|
..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.");
|
logSafe("Form fields initialized.");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -83,9 +96,13 @@ class ReportTaskController extends MyController {
|
|||||||
required DateTime reportedDate,
|
required DateTime reportedDate,
|
||||||
List<File>? images,
|
List<File>? images,
|
||||||
}) async {
|
}) async {
|
||||||
logSafe("Reporting task for projectId", );
|
logSafe(
|
||||||
|
"Reporting task for projectId",
|
||||||
|
);
|
||||||
final completedWork = completedWorkController.text.trim();
|
final completedWork = completedWorkController.text.trim();
|
||||||
if (completedWork.isEmpty || int.tryParse(completedWork) == null || int.parse(completedWork) < 0) {
|
if (completedWork.isEmpty ||
|
||||||
|
int.tryParse(completedWork) == null ||
|
||||||
|
int.parse(completedWork) < 0) {
|
||||||
_showError("Completed work must be a positive number.");
|
_showError("Completed work must be a positive number.");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -121,7 +138,8 @@ class ReportTaskController extends MyController {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
logSafe("Exception while reporting task", level: LogLevel.error, error: e, stackTrace: s);
|
logSafe("Exception while reporting task",
|
||||||
|
level: LogLevel.error, error: e, stackTrace: s);
|
||||||
reportStatus.value = ApiStatus.failure;
|
reportStatus.value = ApiStatus.failure;
|
||||||
_showError("An error occurred while reporting the task.");
|
_showError("An error occurred while reporting the task.");
|
||||||
return false;
|
return false;
|
||||||
@ -138,7 +156,9 @@ class ReportTaskController extends MyController {
|
|||||||
required String comment,
|
required String comment,
|
||||||
List<File>? images,
|
List<File>? images,
|
||||||
}) async {
|
}) async {
|
||||||
logSafe("Submitting comment for project", );
|
logSafe(
|
||||||
|
"Submitting comment for project",
|
||||||
|
);
|
||||||
|
|
||||||
final commentField = commentController.text.trim();
|
final commentField = commentController.text.trim();
|
||||||
if (commentField.isEmpty) {
|
if (commentField.isEmpty) {
|
||||||
@ -166,14 +186,16 @@ class ReportTaskController extends MyController {
|
|||||||
_showError("Failed to comment task.");
|
_showError("Failed to comment task.");
|
||||||
}
|
}
|
||||||
} catch (e, s) {
|
} catch (e, s) {
|
||||||
logSafe("Exception while commenting task", level: LogLevel.error, error: e, stackTrace: s);
|
logSafe("Exception while commenting task",
|
||||||
|
level: LogLevel.error, error: e, stackTrace: s);
|
||||||
_showError("An error occurred while commenting the task.");
|
_showError("An error occurred while commenting the task.");
|
||||||
} finally {
|
} finally {
|
||||||
isLoading.value = false;
|
isLoading.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<Map<String, dynamic>>?> _prepareImages(List<File>? images, String context) async {
|
Future<List<Map<String, dynamic>>?> _prepareImages(
|
||||||
|
List<File>? images, String context) async {
|
||||||
if (images == null || images.isEmpty) return null;
|
if (images == null || images.isEmpty) return null;
|
||||||
|
|
||||||
logSafe("Preparing images for $context upload...");
|
logSafe("Preparing images for $context upload...");
|
||||||
@ -191,7 +213,8 @@ class ReportTaskController extends MyController {
|
|||||||
"description": "Image uploaded for $context",
|
"description": "Image uploaded for $context",
|
||||||
};
|
};
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logSafe("Image processing failed: ${file.path}", level: LogLevel.warning, error: e);
|
logSafe("Image processing failed: ${file.path}",
|
||||||
|
level: LogLevel.warning, error: e);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
@ -212,18 +235,31 @@ class ReportTaskController extends MyController {
|
|||||||
|
|
||||||
Future<void> pickImages({required bool fromCamera}) async {
|
Future<void> pickImages({required bool fromCamera}) async {
|
||||||
try {
|
try {
|
||||||
|
isPickingImage.value = true; // Start loading
|
||||||
|
|
||||||
if (fromCamera) {
|
if (fromCamera) {
|
||||||
final pickedFile = await _picker.pickImage(source: ImageSource.camera, imageQuality: 75);
|
final pickedFile = await _picker.pickImage(
|
||||||
|
source: ImageSource.camera,
|
||||||
|
imageQuality: 75,
|
||||||
|
);
|
||||||
if (pickedFile != null) {
|
if (pickedFile != null) {
|
||||||
selectedImages.add(File(pickedFile.path));
|
// Only camera images get timestamp
|
||||||
|
final timestampedFile = await TimestampImageHelper.addTimestamp(
|
||||||
|
imageFile: File(pickedFile.path),
|
||||||
|
);
|
||||||
|
selectedImages.add(timestampedFile);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
final pickedFiles = await _picker.pickMultiImage(imageQuality: 75);
|
final pickedFiles = await _picker.pickMultiImage(imageQuality: 75);
|
||||||
|
// Gallery images added as-is without timestamp
|
||||||
selectedImages.addAll(pickedFiles.map((xfile) => File(xfile.path)));
|
selectedImages.addAll(pickedFiles.map((xfile) => File(xfile.path)));
|
||||||
}
|
}
|
||||||
logSafe("Images picked: ${selectedImages.length}", );
|
|
||||||
|
logSafe("Images picked: ${selectedImages.length}");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logSafe("Error picking images", level: LogLevel.warning, error: e);
|
logSafe("Error picking images", level: LogLevel.warning, error: e);
|
||||||
|
} finally {
|
||||||
|
isPickingImage.value = false; // Stop loading
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
class ApiEndpoints {
|
class ApiEndpoints {
|
||||||
static const String baseUrl = "https://stageapi.marcoaiot.com/api";
|
// static const String baseUrl = "https://stageapi.marcoaiot.com/api";
|
||||||
// static const String baseUrl = "https://api.marcoaiot.com/api";
|
static const String baseUrl = "https://api.marcoaiot.com/api";
|
||||||
// static const String baseUrl = "https://devapi.marcoaiot.com/api";
|
// static const String baseUrl = "https://devapi.marcoaiot.com/api";
|
||||||
|
|
||||||
// Dashboard Module API Endpoints
|
// Dashboard Module API Endpoints
|
||||||
|
|||||||
97
lib/helpers/widgets/time_stamp_image_helper.dart
Normal file
97
lib/helpers/widgets/time_stamp_image_helper.dart
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
import 'dart:ui' as ui;
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
import 'package:marco/helpers/services/app_logger.dart';
|
||||||
|
|
||||||
|
class TimestampImageHelper {
|
||||||
|
/// Adds a timestamp to an image file and returns a new File
|
||||||
|
static Future<File> addTimestamp({
|
||||||
|
required File imageFile,
|
||||||
|
Color textColor = Colors.white,
|
||||||
|
double fontSize = 60,
|
||||||
|
Color backgroundColor = Colors.black54,
|
||||||
|
double padding = 40,
|
||||||
|
double bottomPadding = 60,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
// Read the image file
|
||||||
|
final bytes = await imageFile.readAsBytes();
|
||||||
|
final originalImage = await decodeImageFromList(bytes);
|
||||||
|
|
||||||
|
// Create a canvas
|
||||||
|
final recorder = ui.PictureRecorder();
|
||||||
|
final canvas = Canvas(recorder);
|
||||||
|
|
||||||
|
// Draw original image
|
||||||
|
final paint = Paint();
|
||||||
|
canvas.drawImage(originalImage, Offset.zero, paint);
|
||||||
|
|
||||||
|
// Timestamp text
|
||||||
|
final now = DateTime.now();
|
||||||
|
final timestamp = DateFormat('dd MMM yyyy hh:mm:ss a').format(now);
|
||||||
|
|
||||||
|
final textStyle = ui.TextStyle(
|
||||||
|
color: textColor,
|
||||||
|
fontSize: fontSize,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
shadows: [
|
||||||
|
const ui.Shadow(
|
||||||
|
color: Colors.black,
|
||||||
|
offset: Offset(3, 3),
|
||||||
|
blurRadius: 6,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
final paragraphStyle = ui.ParagraphStyle(textAlign: TextAlign.left);
|
||||||
|
final paragraphBuilder = ui.ParagraphBuilder(paragraphStyle)
|
||||||
|
..pushStyle(textStyle)
|
||||||
|
..addText(timestamp);
|
||||||
|
|
||||||
|
final paragraph = paragraphBuilder.build();
|
||||||
|
paragraph.layout(const ui.ParagraphConstraints(width: double.infinity));
|
||||||
|
|
||||||
|
final textWidth = paragraph.maxIntrinsicWidth;
|
||||||
|
final yPosition = originalImage.height - paragraph.height - bottomPadding;
|
||||||
|
final xPosition = (originalImage.width - textWidth) / 2;
|
||||||
|
|
||||||
|
// Draw background
|
||||||
|
final backgroundPaint = Paint()
|
||||||
|
..color = backgroundColor
|
||||||
|
..style = PaintingStyle.fill;
|
||||||
|
|
||||||
|
final backgroundRect = Rect.fromLTWH(
|
||||||
|
xPosition - padding,
|
||||||
|
yPosition - 15,
|
||||||
|
textWidth + padding * 2,
|
||||||
|
paragraph.height + 30,
|
||||||
|
);
|
||||||
|
|
||||||
|
canvas.drawRRect(
|
||||||
|
RRect.fromRectAndRadius(backgroundRect, const Radius.circular(8)),
|
||||||
|
backgroundPaint,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Draw timestamp text
|
||||||
|
canvas.drawParagraph(paragraph, Offset(xPosition, yPosition));
|
||||||
|
|
||||||
|
// Convert canvas to image
|
||||||
|
final picture = recorder.endRecording();
|
||||||
|
final img = await picture.toImage(originalImage.width, originalImage.height);
|
||||||
|
|
||||||
|
final byteData = await img.toByteData(format: ui.ImageByteFormat.png);
|
||||||
|
final buffer = byteData!.buffer.asUint8List();
|
||||||
|
|
||||||
|
// Save to temporary file
|
||||||
|
final tempDir = await Directory.systemTemp.createTemp();
|
||||||
|
final timestampedFile = File('${tempDir.path}/timestamped_${DateTime.now().millisecondsSinceEpoch}.png');
|
||||||
|
await timestampedFile.writeAsBytes(buffer);
|
||||||
|
|
||||||
|
return timestampedFile;
|
||||||
|
} catch (e, stacktrace) {
|
||||||
|
logSafe("Error adding timestamp to image", level: LogLevel.error, error: e, stackTrace: stacktrace);
|
||||||
|
return imageFile; // fallback
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -281,11 +281,30 @@ class _CommentTaskBottomSheetState extends State<CommentTaskBottomSheet>
|
|||||||
_buildSectionHeader("Attach Photos", Icons.camera_alt_outlined),
|
_buildSectionHeader("Attach Photos", Icons.camera_alt_outlined),
|
||||||
MySpacing.height(12),
|
MySpacing.height(12),
|
||||||
Obx(() {
|
Obx(() {
|
||||||
|
if (controller.isPickingImage.value) {
|
||||||
|
return Center(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
CircularProgressIndicator(
|
||||||
|
color: Colors.blueAccent,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
"Processing image, please wait...",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
color: Colors.blueAccent,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
final images = controller.selectedImages;
|
final images = controller.selectedImages;
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
// --- Refactoring Note ---
|
|
||||||
// Using the reusable _ImageHorizontalListView for picked images.
|
|
||||||
_ImageHorizontalListView(
|
_ImageHorizontalListView(
|
||||||
imageSources: images.toList(),
|
imageSources: images.toList(),
|
||||||
onPreview: (index) => _showImageViewer(images.toList(), index),
|
onPreview: (index) => _showImageViewer(images.toList(), index),
|
||||||
@ -299,8 +318,11 @@ class _CommentTaskBottomSheetState extends State<CommentTaskBottomSheet>
|
|||||||
color: Colors.grey.shade100,
|
color: Colors.grey.shade100,
|
||||||
),
|
),
|
||||||
child: Center(
|
child: Center(
|
||||||
child: Icon(Icons.photo_library_outlined,
|
child: Icon(
|
||||||
size: 36, color: Colors.grey.shade400),
|
Icons.photo_library_outlined,
|
||||||
|
size: 36,
|
||||||
|
color: Colors.grey.shade400,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@ -290,6 +290,7 @@ class _ReportActionBottomSheetState extends State<ReportActionBottomSheet>
|
|||||||
onCameraTap: () => controller.pickImages(fromCamera: true),
|
onCameraTap: () => controller.pickImages(fromCamera: true),
|
||||||
onUploadTap: () => controller.pickImages(fromCamera: false),
|
onUploadTap: () => controller.pickImages(fromCamera: false),
|
||||||
onRemoveImage: (index) => controller.removeImageAt(index),
|
onRemoveImage: (index) => controller.removeImageAt(index),
|
||||||
|
isProcessing: controller.isPickingImage.value,
|
||||||
onPreviewImage: (index) => showDialog(
|
onPreviewImage: (index) => showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (_) => ImageViewerDialog(
|
builder: (_) => ImageViewerDialog(
|
||||||
|
|||||||
@ -92,17 +92,48 @@ Widget buildReportedImagesSection({
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Local image picker preview (with file images)
|
/// Local image picker preview (with file images)
|
||||||
|
/// Local image picker preview with processing loader
|
||||||
Widget buildImagePickerSection({
|
Widget buildImagePickerSection({
|
||||||
required List<File> images,
|
required List<File> images,
|
||||||
required VoidCallback onCameraTap,
|
required VoidCallback onCameraTap,
|
||||||
required VoidCallback onUploadTap,
|
required VoidCallback onUploadTap,
|
||||||
required void Function(int index) onRemoveImage,
|
required void Function(int index) onRemoveImage,
|
||||||
required void Function(int initialIndex) onPreviewImage,
|
required void Function(int initialIndex) onPreviewImage,
|
||||||
|
required bool isProcessing, // New: show loader while image is being processed
|
||||||
}) {
|
}) {
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
if (images.isEmpty)
|
// Loader placeholder when processing new images
|
||||||
|
if (isProcessing)
|
||||||
|
Container(
|
||||||
|
height: 70,
|
||||||
|
width: double.infinity,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
border: Border.all(color: Colors.grey.shade300, width: 2),
|
||||||
|
color: Colors.grey.shade100,
|
||||||
|
),
|
||||||
|
child: Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
child: CircularProgressIndicator(color: Colors.indigo),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
Text(
|
||||||
|
"Processing image, please wait...",
|
||||||
|
style: TextStyle(fontSize: 12, color: Colors.indigo),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else if (images.isEmpty)
|
||||||
|
// Empty placeholder when no images
|
||||||
Container(
|
Container(
|
||||||
height: 70,
|
height: 70,
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
@ -117,6 +148,7 @@ Widget buildImagePickerSection({
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
|
// Display selected images
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: 70,
|
height: 70,
|
||||||
child: ListView.separated(
|
child: ListView.separated(
|
||||||
@ -160,6 +192,8 @@ Widget buildImagePickerSection({
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
MySpacing.height(16),
|
MySpacing.height(16),
|
||||||
|
|
||||||
|
// Camera & Upload Buttons
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
@ -200,8 +234,8 @@ Widget buildImagePickerSection({
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Comment list widget
|
/// Comment list widget
|
||||||
Widget buildCommentList(
|
Widget buildCommentList(List<Map<String, dynamic>> comments,
|
||||||
List<Map<String, dynamic>> comments, BuildContext context, String Function(String) timeAgo) {
|
BuildContext context, String Function(String) timeAgo) {
|
||||||
comments.sort((a, b) {
|
comments.sort((a, b) {
|
||||||
final aDate = DateTime.tryParse(a['date'] ?? '') ??
|
final aDate = DateTime.tryParse(a['date'] ?? '') ??
|
||||||
DateTime.fromMillisecondsSinceEpoch(0);
|
DateTime.fromMillisecondsSinceEpoch(0);
|
||||||
|
|||||||
@ -52,9 +52,8 @@ class _ReportTaskBottomSheetState extends State<ReportTaskBottomSheet>
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Obx(() {
|
return Obx(() => BaseBottomSheet(
|
||||||
return BaseBottomSheet(
|
|
||||||
title: "Report Task",
|
title: "Report Task",
|
||||||
isSubmitting: controller.reportStatus.value == ApiStatus.loading,
|
isSubmitting: controller.reportStatus.value == ApiStatus.loading,
|
||||||
onCancel: () => Navigator.of(context).pop(),
|
onCancel: () => Navigator.of(context).pop(),
|
||||||
@ -64,25 +63,33 @@ class _ReportTaskBottomSheetState extends State<ReportTaskBottomSheet>
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
_buildRow("Assigned Date", controller.basicValidator.getController('assigned_date')?.text),
|
_buildRow(
|
||||||
_buildRow("Assigned By", controller.basicValidator.getController('assigned_by')?.text),
|
"Assigned Date",
|
||||||
_buildRow("Work Area", controller.basicValidator.getController('work_area')?.text),
|
controller.basicValidator
|
||||||
_buildRow("Activity", controller.basicValidator.getController('activity')?.text),
|
.getController('assigned_date')
|
||||||
_buildRow("Team Size", controller.basicValidator.getController('team_size')?.text),
|
?.text),
|
||||||
|
_buildRow("Assigned By",
|
||||||
|
controller.basicValidator.getController('assigned_by')?.text),
|
||||||
|
_buildRow("Work Area",
|
||||||
|
controller.basicValidator.getController('work_area')?.text),
|
||||||
|
_buildRow("Activity",
|
||||||
|
controller.basicValidator.getController('activity')?.text),
|
||||||
|
_buildRow("Team Size",
|
||||||
|
controller.basicValidator.getController('team_size')?.text),
|
||||||
_buildRow(
|
_buildRow(
|
||||||
"Assigned",
|
"Assigned",
|
||||||
"${controller.basicValidator.getController('assigned')?.text ?? '-'} "
|
"${controller.basicValidator.getController('assigned')?.text ?? '-'} "
|
||||||
"of ${widget.taskData['pendingWork'] ?? '-'} Pending",
|
"of ${widget.taskData['pendingWork'] ?? '-'} Pending",
|
||||||
),
|
),
|
||||||
_buildCompletedWorkField(),
|
_buildCompletedWorkField(),
|
||||||
_buildCommentField(),
|
_buildCommentField(),
|
||||||
Obx(() => _buildImageSection()),
|
_buildImageSection(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
));
|
||||||
});
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _handleSubmit() async {
|
Future<void> _handleSubmit() async {
|
||||||
final v = controller.basicValidator;
|
final v = controller.basicValidator;
|
||||||
@ -91,7 +98,8 @@ class _ReportTaskBottomSheetState extends State<ReportTaskBottomSheet>
|
|||||||
final success = await controller.reportTask(
|
final success = await controller.reportTask(
|
||||||
projectId: v.getController('task_id')?.text ?? '',
|
projectId: v.getController('task_id')?.text ?? '',
|
||||||
comment: v.getController('comment')?.text ?? '',
|
comment: v.getController('comment')?.text ?? '',
|
||||||
completedTask: int.tryParse(v.getController('completed_work')?.text ?? '') ?? 0,
|
completedTask:
|
||||||
|
int.tryParse(v.getController('completed_work')?.text ?? '') ?? 0,
|
||||||
checklist: [],
|
checklist: [],
|
||||||
reportedDate: DateTime.now(),
|
reportedDate: DateTime.now(),
|
||||||
images: controller.selectedImages,
|
images: controller.selectedImages,
|
||||||
@ -118,12 +126,14 @@ class _ReportTaskBottomSheetState extends State<ReportTaskBottomSheet>
|
|||||||
child: Row(
|
child: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Icon(icons[label] ?? Icons.info_outline, size: 18, color: Colors.grey[700]),
|
Icon(icons[label] ?? Icons.info_outline,
|
||||||
|
size: 18, color: Colors.grey[700]),
|
||||||
MySpacing.width(8),
|
MySpacing.width(8),
|
||||||
MyText.titleSmall("$label:", fontWeight: 600),
|
MyText.titleSmall("$label:", fontWeight: 600),
|
||||||
MySpacing.width(12),
|
MySpacing.width(12),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: MyText.bodyMedium(value?.trim().isNotEmpty == true ? value!.trim() : "-"),
|
child: MyText.bodyMedium(
|
||||||
|
value?.trim().isNotEmpty == true ? value!.trim() : "-"),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -148,10 +158,12 @@ class _ReportTaskBottomSheetState extends State<ReportTaskBottomSheet>
|
|||||||
controller: controller.basicValidator.getController('completed_work'),
|
controller: controller.basicValidator.getController('completed_work'),
|
||||||
keyboardType: TextInputType.number,
|
keyboardType: TextInputType.number,
|
||||||
validator: (value) {
|
validator: (value) {
|
||||||
if (value == null || value.trim().isEmpty) return 'Please enter completed work';
|
if (value == null || value.trim().isEmpty)
|
||||||
|
return 'Please enter completed work';
|
||||||
final completed = int.tryParse(value.trim());
|
final completed = int.tryParse(value.trim());
|
||||||
if (completed == null) return 'Enter a valid number';
|
if (completed == null) return 'Enter a valid number';
|
||||||
if (completed > pending) return 'Completed work cannot exceed pending work $pending';
|
if (completed > pending)
|
||||||
|
return 'Completed work cannot exceed pending work $pending';
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
@ -203,8 +215,6 @@ class _ReportTaskBottomSheetState extends State<ReportTaskBottomSheet>
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildImageSection() {
|
Widget _buildImageSection() {
|
||||||
final images = controller.selectedImages;
|
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
@ -216,94 +226,130 @@ class _ReportTaskBottomSheetState extends State<ReportTaskBottomSheet>
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
MySpacing.height(12),
|
MySpacing.height(12),
|
||||||
if (images.isEmpty)
|
// Only wrap reactive parts with Obx
|
||||||
Container(
|
Obx(() {
|
||||||
height: 70,
|
if (controller.isPickingImage.value) {
|
||||||
width: double.infinity,
|
return Center(
|
||||||
decoration: BoxDecoration(
|
child: Column(
|
||||||
borderRadius: BorderRadius.circular(12),
|
children: [
|
||||||
border: Border.all(color: Colors.grey.shade300, width: 2),
|
CircularProgressIndicator(color: Colors.blueAccent),
|
||||||
color: Colors.grey.shade100,
|
const SizedBox(height: 8),
|
||||||
),
|
Text(
|
||||||
child: Center(
|
"Processing image, please wait...",
|
||||||
child: Icon(Icons.photo_camera_outlined, size: 48, color: Colors.grey.shade400),
|
style: TextStyle(
|
||||||
),
|
fontSize: 14,
|
||||||
)
|
color: Colors.blueAccent,
|
||||||
else
|
),
|
||||||
SizedBox(
|
),
|
||||||
height: 70,
|
],
|
||||||
child: ListView.separated(
|
),
|
||||||
scrollDirection: Axis.horizontal,
|
);
|
||||||
itemCount: images.length,
|
}
|
||||||
separatorBuilder: (_, __) => MySpacing.width(12),
|
|
||||||
itemBuilder: (context, index) {
|
final images = controller.selectedImages;
|
||||||
final file = images[index];
|
|
||||||
return Stack(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
GestureDetector(
|
if (images.isEmpty)
|
||||||
onTap: () {
|
Container(
|
||||||
showDialog(
|
height: 70,
|
||||||
context: context,
|
width: double.infinity,
|
||||||
builder: (_) => Dialog(
|
decoration: BoxDecoration(
|
||||||
child: InteractiveViewer(child: Image.file(file)),
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
border: Border.all(color: Colors.grey.shade300, width: 2),
|
||||||
|
color: Colors.grey.shade100,
|
||||||
|
),
|
||||||
|
child: Center(
|
||||||
|
child: Icon(Icons.photo_camera_outlined,
|
||||||
|
size: 48, color: Colors.grey.shade400),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
SizedBox(
|
||||||
|
height: 70,
|
||||||
|
child: ListView.separated(
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
itemCount: images.length,
|
||||||
|
separatorBuilder: (_, __) => MySpacing.width(12),
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final file = images[index];
|
||||||
|
return Stack(
|
||||||
|
children: [
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (_) => Dialog(
|
||||||
|
child: InteractiveViewer(
|
||||||
|
child: Image.file(file)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
child: Image.file(file,
|
||||||
|
height: 70, width: 70, fit: BoxFit.cover),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
Positioned(
|
||||||
},
|
top: 4,
|
||||||
child: ClipRRect(
|
right: 4,
|
||||||
borderRadius: BorderRadius.circular(12),
|
child: GestureDetector(
|
||||||
child: Image.file(file, height: 70, width: 70, fit: BoxFit.cover),
|
onTap: () =>
|
||||||
|
controller.selectedImages.removeAt(index),
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.black54,
|
||||||
|
shape: BoxShape.circle),
|
||||||
|
child: const Icon(Icons.close,
|
||||||
|
size: 20, color: Colors.white),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
MySpacing.height(16),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: MyButton.outlined(
|
||||||
|
onPressed: () => controller.pickImages(fromCamera: true),
|
||||||
|
padding: MySpacing.xy(12, 10),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.camera_alt,
|
||||||
|
size: 16, color: Colors.blueAccent),
|
||||||
|
MySpacing.width(6),
|
||||||
|
MyText.bodySmall('Capture', color: Colors.blueAccent),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Positioned(
|
),
|
||||||
top: 4,
|
MySpacing.width(12),
|
||||||
right: 4,
|
Expanded(
|
||||||
child: GestureDetector(
|
child: MyButton.outlined(
|
||||||
onTap: () => controller.removeImageAt(index),
|
onPressed: () => controller.pickImages(fromCamera: false),
|
||||||
child: Container(
|
padding: MySpacing.xy(12, 10),
|
||||||
decoration: BoxDecoration(color: Colors.black54, shape: BoxShape.circle),
|
child: Row(
|
||||||
child: const Icon(Icons.close, size: 20, color: Colors.white),
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
),
|
children: [
|
||||||
|
const Icon(Icons.upload_file,
|
||||||
|
size: 16, color: Colors.blueAccent),
|
||||||
|
MySpacing.width(6),
|
||||||
|
MyText.bodySmall('Upload', color: Colors.blueAccent),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
);
|
],
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
MySpacing.height(16),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: MyButton.outlined(
|
|
||||||
onPressed: () => controller.pickImages(fromCamera: true),
|
|
||||||
padding: MySpacing.xy(12, 10),
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
const Icon(Icons.camera_alt, size: 16, color: Colors.blueAccent),
|
|
||||||
MySpacing.width(6),
|
|
||||||
MyText.bodySmall('Capture', color: Colors.blueAccent),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
MySpacing.width(12),
|
);
|
||||||
Expanded(
|
}),
|
||||||
child: MyButton.outlined(
|
|
||||||
onPressed: () => controller.pickImages(fromCamera: false),
|
|
||||||
padding: MySpacing.xy(12, 10),
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
const Icon(Icons.upload_file, size: 16, color: Colors.blueAccent),
|
|
||||||
MySpacing.width(6),
|
|
||||||
MyText.bodySmall('Upload', color: Colors.blueAccent),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
|
||||||
import 'package:marco/controller/expense/add_expense_controller.dart';
|
import 'package:marco/controller/expense/add_expense_controller.dart';
|
||||||
|
import 'package:marco/helpers/utils/mixins/ui_mixin.dart';
|
||||||
import 'package:marco/model/expense/expense_type_model.dart';
|
import 'package:marco/model/expense/expense_type_model.dart';
|
||||||
import 'package:marco/model/expense/payment_types_model.dart';
|
import 'package:marco/model/expense/payment_types_model.dart';
|
||||||
import 'package:marco/model/expense/employee_selector_bottom_sheet.dart';
|
import 'package:marco/model/expense/employee_selector_bottom_sheet.dart';
|
||||||
@ -40,7 +41,8 @@ class _AddExpenseBottomSheet extends StatefulWidget {
|
|||||||
State<_AddExpenseBottomSheet> createState() => _AddExpenseBottomSheetState();
|
State<_AddExpenseBottomSheet> createState() => _AddExpenseBottomSheetState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _AddExpenseBottomSheetState extends State<_AddExpenseBottomSheet> {
|
class _AddExpenseBottomSheetState extends State<_AddExpenseBottomSheet>
|
||||||
|
with UIMixin {
|
||||||
final AddExpenseController controller = Get.put(AddExpenseController());
|
final AddExpenseController controller = Get.put(AddExpenseController());
|
||||||
final _formKey = GlobalKey<FormState>();
|
final _formKey = GlobalKey<FormState>();
|
||||||
|
|
||||||
@ -326,8 +328,7 @@ class _AddExpenseBottomSheetState extends State<_AddExpenseBottomSheet> {
|
|||||||
CustomTextField(
|
CustomTextField(
|
||||||
controller: controller,
|
controller: controller,
|
||||||
hint: hint ?? "",
|
hint: hint ?? "",
|
||||||
keyboardType:
|
keyboardType: keyboardType ?? TextInputType.text,
|
||||||
keyboardType ?? TextInputType.text,
|
|
||||||
validator: validator,
|
validator: validator,
|
||||||
maxLines: maxLines,
|
maxLines: maxLines,
|
||||||
),
|
),
|
||||||
@ -426,39 +427,65 @@ class _AddExpenseBottomSheetState extends State<_AddExpenseBottomSheet> {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
const SectionTitle(
|
const SectionTitle(
|
||||||
icon: Icons.attach_file, title: "Attachments", requiredField: true),
|
icon: Icons.attach_file,
|
||||||
MySpacing.height(6),
|
title: "Attachments",
|
||||||
AttachmentsSection(
|
requiredField: true,
|
||||||
attachments: controller.attachments,
|
),
|
||||||
existingAttachments: controller.existingAttachments,
|
MySpacing.height(10),
|
||||||
onRemoveNew: controller.removeAttachment,
|
Obx(() {
|
||||||
onRemoveExisting: (item) async {
|
if (controller.isProcessingAttachment.value) {
|
||||||
await showDialog(
|
return Center(
|
||||||
context: context,
|
child: Column(
|
||||||
barrierDismissible: false,
|
children: [
|
||||||
builder: (_) => ConfirmDialog(
|
CircularProgressIndicator(
|
||||||
title: "Remove Attachment",
|
color: contentTheme.primary,
|
||||||
message: "Are you sure you want to remove this attachment?",
|
),
|
||||||
confirmText: "Remove",
|
const SizedBox(height: 8),
|
||||||
icon: Icons.delete,
|
Text(
|
||||||
confirmColor: Colors.redAccent,
|
"Processing image, please wait...",
|
||||||
onConfirm: () async {
|
style: TextStyle(
|
||||||
final index = controller.existingAttachments.indexOf(item);
|
fontSize: 14,
|
||||||
if (index != -1) {
|
color: contentTheme.primary,
|
||||||
controller.existingAttachments[index]['isActive'] = false;
|
),
|
||||||
controller.existingAttachments.refresh();
|
),
|
||||||
}
|
],
|
||||||
showAppSnackbar(
|
|
||||||
title: 'Removed',
|
|
||||||
message: 'Attachment has been removed.',
|
|
||||||
type: SnackbarType.success,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
}
|
||||||
onAdd: controller.pickAttachments,
|
|
||||||
),
|
return AttachmentsSection(
|
||||||
|
attachments: controller.attachments,
|
||||||
|
existingAttachments: controller.existingAttachments,
|
||||||
|
onRemoveNew: controller.removeAttachment,
|
||||||
|
onRemoveExisting: (item) async {
|
||||||
|
await showDialog(
|
||||||
|
context: context,
|
||||||
|
barrierDismissible: false,
|
||||||
|
builder: (_) => ConfirmDialog(
|
||||||
|
title: "Remove Attachment",
|
||||||
|
message: "Are you sure you want to remove this attachment?",
|
||||||
|
confirmText: "Remove",
|
||||||
|
icon: Icons.delete,
|
||||||
|
confirmColor: Colors.redAccent,
|
||||||
|
onConfirm: () async {
|
||||||
|
final index = controller.existingAttachments.indexOf(item);
|
||||||
|
if (index != -1) {
|
||||||
|
controller.existingAttachments[index]['isActive'] = false;
|
||||||
|
controller.existingAttachments.refresh();
|
||||||
|
}
|
||||||
|
showAppSnackbar(
|
||||||
|
title: 'Removed',
|
||||||
|
message: 'Attachment has been removed.',
|
||||||
|
type: SnackbarType.success,
|
||||||
|
);
|
||||||
|
Navigator.pop(context);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onAdd: controller.pickAttachments,
|
||||||
|
);
|
||||||
|
}),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user