removed unwanted files

This commit is contained in:
Vaibhav Surve 2025-09-30 17:00:20 +05:30
parent 3515cab0d5
commit 4feb2875f0
33 changed files with 1 additions and 6963 deletions

View File

@ -1,152 +0,0 @@
import 'package:get/get.dart';
import 'package:marco/helpers/services/app_logger.dart';
import 'package:marco/helpers/services/api_service.dart';
import 'package:marco/helpers/widgets/my_form_validator.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart';
import 'package:marco/model/dailyTaskPlanning/master_work_category_model.dart';
class AddTaskController extends GetxController {
RxMap<String, RxBool> uploadingStates = <String, RxBool>{}.obs;
MyFormValidator basicValidator = MyFormValidator();
RxnString selectedCategoryId = RxnString();
RxnString selectedCategoryName = RxnString();
var categoryIdNameMap = <String, String>{}.obs;
List<Map<String, dynamic>> roles = [];
RxnString selectedRoleId = RxnString();
RxBool isLoadingWorkMasterCategories = false.obs;
RxList<WorkCategoryModel> workMasterCategories = <WorkCategoryModel>[].obs;
RxBool isLoading = false.obs;
@override
void onInit() {
super.onInit();
fetchWorkMasterCategories();
}
String? formFieldValidator(String? value, {required String fieldType}) {
if (value == null || value.trim().isEmpty) {
return 'This field is required';
}
if (fieldType == "target" && int.tryParse(value.trim()) == null) {
return 'Please enter a valid number';
}
if (fieldType == "description" && value.trim().length < 5) {
return 'Description must be at least 5 characters';
}
return null;
}
Future<bool> assignDailyTask({
required String workItemId,
required int plannedTask,
required String description,
required List<String> taskTeam,
DateTime? assignmentDate,
}) async {
logSafe("Starting task assignment...", level: LogLevel.info);
final response = await ApiService.assignDailyTask(
workItemId: workItemId,
plannedTask: plannedTask,
description: description,
taskTeam: taskTeam,
assignmentDate: assignmentDate,
);
if (response == true) {
logSafe("Task assigned successfully.", level: LogLevel.info);
showAppSnackbar(
title: "Success",
message: "Task assigned successfully!",
type: SnackbarType.success,
);
return true;
} else {
logSafe("Failed to assign task.", level: LogLevel.error);
showAppSnackbar(
title: "Error",
message: "Failed to assign task.",
type: SnackbarType.error,
);
return false;
}
}
Future<bool> createTask({
required String parentTaskId,
required String workAreaId,
required String activityId,
required int plannedTask,
required String comment,
required String categoryId,
DateTime? assignmentDate,
}) async {
logSafe("Creating new task...", level: LogLevel.info);
final response = await ApiService.createTask(
parentTaskId: parentTaskId,
plannedTask: plannedTask,
comment: comment,
workAreaId: workAreaId,
activityId: activityId,
assignmentDate: assignmentDate,
categoryId: categoryId,
);
if (response == true) {
logSafe("Task created successfully.", level: LogLevel.info);
showAppSnackbar(
title: "Success",
message: "Task created successfully!",
type: SnackbarType.success,
);
return true;
} else {
logSafe("Failed to create task.", level: LogLevel.error);
showAppSnackbar(
title: "Error",
message: "Failed to create task.",
type: SnackbarType.error,
);
return false;
}
}
Future<void> fetchWorkMasterCategories() async {
isLoadingWorkMasterCategories.value = true;
try {
final response = await ApiService.getMasterWorkCategories();
if (response != null) {
final dataList = response['data'] ?? [];
final parsedList = List<WorkCategoryModel>.from(
dataList.map((e) => WorkCategoryModel.fromJson(e)),
);
workMasterCategories.assignAll(parsedList);
final mapped = {for (var item in parsedList) item.id: item.name};
categoryIdNameMap.assignAll(mapped);
logSafe("Work categories fetched: ${dataList.length}", level: LogLevel.info);
} else {
logSafe("No work categories found or API call failed.", level: LogLevel.warning);
}
} catch (e, st) {
logSafe("Error parsing work categories", level: LogLevel.error, error: e, stackTrace: st);
workMasterCategories.clear();
categoryIdNameMap.clear();
}
isLoadingWorkMasterCategories.value = false;
update();
}
void selectCategory(String id) {
selectedCategoryId.value = id;
selectedCategoryName.value = categoryIdNameMap[id];
logSafe("Category selected", level: LogLevel.debug, );
}
}

View File

@ -1,146 +0,0 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:marco/helpers/services/app_logger.dart';
import 'package:marco/helpers/services/api_service.dart';
import 'package:marco/model/project_model.dart';
import 'package:marco/model/dailyTaskPlanning/daily_task_model.dart';
class DailyTaskController extends GetxController {
List<ProjectModel> projects = [];
String? selectedProjectId;
DateTime? startDateTask;
DateTime? endDateTask;
List<TaskModel> dailyTasks = [];
final RxSet<String> expandedDates = <String>{}.obs;
void toggleDate(String dateKey) {
if (expandedDates.contains(dateKey)) {
expandedDates.remove(dateKey);
} else {
expandedDates.add(dateKey);
}
}
RxBool isLoading = true.obs;
RxBool isLoadingMore = false.obs;
Map<String, List<TaskModel>> groupedDailyTasks = {};
// Pagination
int currentPage = 1;
int pageSize = 20;
bool hasMore = true;
@override
void onInit() {
super.onInit();
_initializeDefaults();
}
void _initializeDefaults() {
_setDefaultDateRange();
}
void _setDefaultDateRange() {
final today = DateTime.now();
startDateTask = today.subtract(const Duration(days: 7));
endDateTask = today;
logSafe(
"Default date range set: $startDateTask to $endDateTask",
level: LogLevel.info,
);
}
Future<void> fetchTaskData(
String projectId, {
List<String>? serviceIds,
int pageNumber = 1,
int pageSize = 20,
bool isLoadMore = false,
}) async {
if (!isLoadMore) {
isLoading.value = true;
currentPage = 1;
hasMore = true;
groupedDailyTasks.clear();
dailyTasks.clear();
} else {
isLoadingMore.value = true;
}
final response = await ApiService.getDailyTasks(
projectId,
dateFrom: startDateTask,
dateTo: endDateTask,
serviceIds: serviceIds,
pageNumber: pageNumber,
pageSize: pageSize,
);
if (response != null && response.isNotEmpty) {
for (var taskJson in response) {
final task = TaskModel.fromJson(taskJson);
final assignmentDateKey =
task.assignmentDate.toIso8601String().split('T')[0];
groupedDailyTasks.putIfAbsent(assignmentDateKey, () => []).add(task);
}
dailyTasks = groupedDailyTasks.values.expand((list) => list).toList();
currentPage = pageNumber;
} else {
hasMore = false;
}
isLoading.value = false;
isLoadingMore.value = false;
update();
}
Future<void> selectDateRangeForTaskData(
BuildContext context,
DailyTaskController controller,
) async {
final picked = await showDateRangePicker(
context: context,
firstDate: DateTime(2022),
lastDate: DateTime.now(),
initialDateRange: DateTimeRange(
start:
startDateTask ?? DateTime.now().subtract(const Duration(days: 7)),
end: endDateTask ?? DateTime.now(),
),
);
if (picked == null) {
logSafe("Date range picker cancelled by user.", level: LogLevel.debug);
return;
}
startDateTask = picked.start;
endDateTask = picked.end;
logSafe(
"Date range selected: $startDateTask to $endDateTask",
level: LogLevel.info,
);
// Add null check before calling fetchTaskData
final projectId = controller.selectedProjectId;
if (projectId != null && projectId.isNotEmpty) {
await controller.fetchTaskData(projectId);
} else {
logSafe("Project ID is null or empty, skipping fetchTaskData",
level: LogLevel.warning);
}
}
void refreshTasksFromNotification({
required String projectId,
required String taskAllocationId,
}) async {
// re-fetch tasks
await fetchTaskData(projectId);
update(); // rebuilds UI
}
}

View File

@ -1,279 +0,0 @@
import 'package:get/get.dart';
import 'package:marco/helpers/services/app_logger.dart';
import 'package:marco/helpers/services/api_service.dart';
import 'package:marco/helpers/widgets/my_form_validator.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart';
import 'package:marco/model/project_model.dart';
import 'package:marco/model/dailyTaskPlanning/daily_task_planning_model.dart';
import 'package:marco/model/employees/employee_model.dart';
class DailyTaskPlanningController extends GetxController {
List<ProjectModel> projects = [];
List<EmployeeModel> employees = [];
List<TaskPlanningDetailsModel> dailyTasks = [];
RxMap<String, RxBool> uploadingStates = <String, RxBool>{}.obs;
RxList<EmployeeModel> selectedEmployees = <EmployeeModel>[].obs;
MyFormValidator basicValidator = MyFormValidator();
List<Map<String, dynamic>> roles = [];
RxBool isAssigningTask = false.obs;
RxnString selectedRoleId = RxnString();
RxBool isLoading = false.obs;
@override
void onInit() {
super.onInit();
fetchRoles();
_initializeDefaults();
}
void _initializeDefaults() {
fetchProjects();
}
String? formFieldValidator(String? value, {required String fieldType}) {
if (value == null || value.trim().isEmpty) {
return 'This field is required';
}
if (fieldType == "target" && int.tryParse(value.trim()) == null) {
return 'Please enter a valid number';
}
if (fieldType == "description" && value.trim().length < 5) {
return 'Description must be at least 5 characters';
}
return null;
}
void updateSelectedEmployees() {
final selected =
employees.where((e) => uploadingStates[e.id]?.value == true).toList();
selectedEmployees.value = selected;
logSafe("Updated selected employees", level: LogLevel.debug);
}
void onRoleSelected(String? roleId) {
selectedRoleId.value = roleId;
logSafe("Role selected", level: LogLevel.info);
}
Future<void> fetchRoles() async {
logSafe("Fetching roles...", level: LogLevel.info);
final result = await ApiService.getRoles();
if (result != null) {
roles = List<Map<String, dynamic>>.from(result);
logSafe("Roles fetched successfully", level: LogLevel.info);
update();
} else {
logSafe("Failed to fetch roles", level: LogLevel.error);
}
}
Future<bool> assignDailyTask({
required String workItemId,
required int plannedTask,
required String description,
required List<String> taskTeam,
DateTime? assignmentDate,
}) async {
isAssigningTask.value = true;
logSafe("Starting assign task...", level: LogLevel.info);
final response = await ApiService.assignDailyTask(
workItemId: workItemId,
plannedTask: plannedTask,
description: description,
taskTeam: taskTeam,
assignmentDate: assignmentDate,
);
isAssigningTask.value = false;
if (response == true) {
logSafe("Task assigned successfully", level: LogLevel.info);
showAppSnackbar(
title: "Success",
message: "Task assigned successfully!",
type: SnackbarType.success,
);
return true;
} else {
logSafe("Failed to assign task", level: LogLevel.error);
showAppSnackbar(
title: "Error",
message: "Failed to assign task.",
type: SnackbarType.error,
);
return false;
}
}
Future<void> fetchProjects() async {
isLoading.value = true;
try {
final response = await ApiService.getProjects();
if (response?.isEmpty ?? true) {
logSafe("No project data found or API call failed",
level: LogLevel.warning);
return;
}
projects = response!.map((json) => ProjectModel.fromJson(json)).toList();
logSafe("Projects fetched: ${projects.length} projects loaded",
level: LogLevel.info);
update();
} catch (e, stack) {
logSafe("Error fetching projects",
level: LogLevel.error, error: e, stackTrace: stack);
} finally {
isLoading.value = false;
}
}
/// Fetch Infra details and then tasks per work area
Future<void> fetchTaskData(String? projectId, {String? serviceId}) async {
if (projectId == null) {
logSafe("Project ID is null", level: LogLevel.warning);
return;
}
isLoading.value = true;
try {
// Fetch infra details
final infraResponse = await ApiService.getInfraDetails(projectId);
final infraData = infraResponse?['data'] as List<dynamic>?;
if (infraData == null || infraData.isEmpty) {
logSafe("No infra data found for project $projectId",
level: LogLevel.warning);
dailyTasks = [];
return;
}
// Map infra to dailyTasks structure
dailyTasks = infraData.map((buildingJson) {
final building = Building(
id: buildingJson['id'],
name: buildingJson['buildingName'],
description: buildingJson['description'],
floors: (buildingJson['floors'] as List<dynamic>).map((floorJson) {
return Floor(
id: floorJson['id'],
floorName: floorJson['floorName'],
workAreas:
(floorJson['workAreas'] as List<dynamic>).map((areaJson) {
return WorkArea(
id: areaJson['id'],
areaName: areaJson['areaName'],
workItems: [], // Will fill after tasks API
);
}).toList(),
);
}).toList(),
);
return TaskPlanningDetailsModel(
id: building.id,
name: building.name,
projectAddress: "",
contactPerson: "",
startDate: DateTime.now(),
endDate: DateTime.now(),
projectStatusId: "",
buildings: [building],
);
}).toList();
// Fetch tasks for each work area, passing serviceId only if selected
await Future.wait(dailyTasks
.expand((task) => task.buildings)
.expand((b) => b.floors)
.expand((f) => f.workAreas)
.map((area) async {
try {
final taskResponse = await ApiService.getWorkItemsByWorkArea(
area.id,
// serviceId: serviceId, // <-- only pass if not null
);
final taskData = taskResponse?['data'] as List<dynamic>? ?? [];
area.workItems.addAll(taskData.map((taskJson) {
return WorkItemWrapper(
workItemId: taskJson['id'],
workItem: WorkItem(
id: taskJson['id'],
activityMaster: taskJson['activityMaster'] != null
? ActivityMaster.fromJson(taskJson['activityMaster'])
: null,
workCategoryMaster: taskJson['workCategoryMaster'] != null
? WorkCategoryMaster.fromJson(
taskJson['workCategoryMaster'])
: null,
plannedWork: (taskJson['plannedWork'] as num?)?.toDouble(),
completedWork: (taskJson['completedWork'] as num?)?.toDouble(),
todaysAssigned:
(taskJson['todaysAssigned'] as num?)?.toDouble(),
description: taskJson['description'] as String?,
taskDate: taskJson['taskDate'] != null
? DateTime.tryParse(taskJson['taskDate'])
: null,
),
);
}));
} catch (e, stack) {
logSafe("Error fetching tasks for work area ${area.id}",
level: LogLevel.error, error: e, stackTrace: stack);
}
}));
logSafe("Fetched infra and tasks for project $projectId",
level: LogLevel.info);
} catch (e, stack) {
logSafe("Error fetching daily task data",
level: LogLevel.error, error: e, stackTrace: stack);
} finally {
isLoading.value = false;
update();
}
}
Future<void> fetchEmployeesByProject(String? projectId) async {
if (projectId == null || projectId.isEmpty) {
logSafe("Project ID is required but was null or empty",
level: LogLevel.error);
return;
}
isLoading.value = true;
try {
final response = await ApiService.getAllEmployeesByProject(projectId);
if (response != null && response.isNotEmpty) {
employees =
response.map((json) => EmployeeModel.fromJson(json)).toList();
for (var emp in employees) {
uploadingStates[emp.id] = false.obs;
}
logSafe(
"Employees fetched: ${employees.length} for project $projectId",
level: LogLevel.info,
);
} else {
employees = [];
logSafe(
"No employees found for project $projectId",
level: LogLevel.warning,
);
}
} catch (e, stack) {
logSafe(
"Error fetching employees for project $projectId",
level: LogLevel.error,
error: e,
stackTrace: stack,
);
} finally {
isLoading.value = false;
update();
}
}
}

View File

@ -1,296 +0,0 @@
import 'dart:convert';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:image_picker/image_picker.dart';
import 'package:marco/helpers/services/app_logger.dart';
import 'package:marco/controller/my_controller.dart';
import 'package:marco/controller/task_Planning/daily_task_Planning_controller.dart';
import 'package:marco/helpers/services/api_service.dart';
import 'package:marco/helpers/widgets/my_form_validator.dart';
import 'package:marco/helpers/widgets/my_image_compressor.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart';
import 'package:marco/model/dailyTaskPlanning/work_status_model.dart';
enum ApiStatus { idle, loading, success, failure }
class ReportTaskActionController extends MyController {
final RxBool isLoading = false.obs;
final Rx<ApiStatus> reportStatus = ApiStatus.idle.obs;
final Rx<ApiStatus> commentStatus = ApiStatus.idle.obs;
final RxList<File> selectedImages = <File>[].obs;
final RxList<WorkStatus> workStatus = <WorkStatus>[].obs;
final RxList<WorkStatus> workStatuses = <WorkStatus>[].obs;
final RxBool showAddTaskCheckbox = false.obs;
final RxBool isAddTaskChecked = false.obs;
final RxBool isLoadingWorkStatus = false.obs;
final Rxn<Map<String, dynamic>> selectedTask = Rxn<Map<String, dynamic>>();
final RxString selectedWorkStatusName = ''.obs;
final MyFormValidator basicValidator = MyFormValidator();
final DailyTaskPlanningController taskController = Get.put(DailyTaskPlanningController());
final ImagePicker _picker = ImagePicker();
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();
final approvedTaskController = TextEditingController();
List<TextEditingController> get _allControllers => [
assignedDateController,
workAreaController,
activityController,
teamSizeController,
taskIdController,
assignedController,
completedWorkController,
commentController,
assignedByController,
teamMembersController,
plannedWorkController,
approvedTaskController,
];
@override
void onInit() {
super.onInit();
logSafe("Initializing ReportTaskController...");
_initializeFormFields();
}
@override
void onClose() {
for (final controller in _allControllers) {
controller.dispose();
}
logSafe("Disposed all text controllers in ReportTaskActionController.");
super.onClose();
}
void _initializeFormFields() {
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)
..addField('approved_task', label: "Approved Task", required: true, controller: approvedTaskController);
}
Future<bool> approveTask({
required String projectId,
required String comment,
required String reportActionId,
required String approvedTaskCount,
List<File>? images,
}) async {
logSafe("approveTask() started", sensitive: false);
if (projectId.isEmpty || reportActionId.isEmpty) {
_showError("Project ID and Report Action ID are required.");
logSafe("Missing required projectId or reportActionId", level: LogLevel.warning);
return false;
}
final approvedTaskInt = int.tryParse(approvedTaskCount);
final completedWorkInt = int.tryParse(completedWorkController.text.trim());
if (approvedTaskInt == null) {
_showError("Invalid approved task count.");
logSafe("Invalid approvedTaskCount: $approvedTaskCount", level: LogLevel.warning);
return false;
}
if (completedWorkInt != null && approvedTaskInt > completedWorkInt) {
_showError("Approved task count cannot exceed completed work.");
logSafe("Validation failed: approved > completed", level: LogLevel.warning);
return false;
}
if (comment.trim().isEmpty) {
_showError("Comment is required.");
logSafe("Comment field is empty", level: LogLevel.warning);
return false;
}
try {
reportStatus.value = ApiStatus.loading;
isLoading.value = true;
logSafe("Calling _prepareImages() for approval...");
final imageData = await _prepareImages(images);
logSafe("Calling ApiService.approveTask()");
final success = await ApiService.approveTask(
id: projectId,
workStatus: reportActionId,
approvedTask: approvedTaskInt,
comment: comment,
images: imageData,
);
if (success) {
logSafe("Task approved successfully");
_showSuccess("Task approved successfully!");
await taskController.fetchTaskData(projectId);
return true;
} else {
logSafe("API returned failure on approveTask", level: LogLevel.error);
_showError("Failed to approve task.");
return false;
}
} catch (e, st) {
logSafe("Error in approveTask: $e", level: LogLevel.error, error: e, stackTrace: st);
_showError("An error occurred.");
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("commentTask() started", sensitive: false);
if (commentController.text.trim().isEmpty) {
_showError("Comment is required.");
logSafe("Comment field is empty", level: LogLevel.warning);
return;
}
try {
isLoading.value = true;
logSafe("Calling _prepareImages() for comment...");
final imageData = await _prepareImages(images);
logSafe("Calling ApiService.commentTask()");
final success = await ApiService.commentTask(
id: projectId,
comment: commentController.text.trim(),
images: imageData,
).timeout(const Duration(seconds: 30), onTimeout: () {
throw Exception("Request timed out.");
});
if (success) {
logSafe("Comment added successfully");
_showSuccess("Task commented successfully!");
await taskController.fetchTaskData(projectId);
} else {
logSafe("API returned failure on commentTask", level: LogLevel.error);
_showError("Failed to comment task.");
}
} catch (e, st) {
logSafe("Error in commentTask: $e", level: LogLevel.error, error: e, stackTrace: st);
_showError("An error occurred while commenting the task.");
} finally {
isLoading.value = false;
}
}
Future<void> fetchWorkStatuses() async {
logSafe("Fetching work statuses...");
isLoadingWorkStatus.value = true;
final response = await ApiService.getWorkStatus();
if (response != null) {
final model = WorkStatusResponseModel.fromJson(response);
workStatus.assignAll(model.data);
logSafe("Fetched ${model.data.length} work statuses");
} else {
logSafe("No work statuses found or API call failed", level: LogLevel.warning);
}
isLoadingWorkStatus.value = false;
update(['dashboard_controller']);
}
Future<List<Map<String, dynamic>>?> _prepareImages(List<File>? images) async {
if (images == null || images.isEmpty) {
logSafe("_prepareImages: No images selected.");
return null;
}
logSafe("_prepareImages: Compressing and encoding images...");
final results = await Future.wait(images.map((file) async {
final compressedBytes = await compressImageToUnder100KB(file);
if (compressedBytes == null) return null;
return {
"fileName": file.path.split('/').last,
"base64Data": base64Encode(compressedBytes),
"contentType": _getContentTypeFromFileName(file.path),
"fileSize": compressedBytes.lengthInBytes,
"description": "Image uploaded for task",
};
}));
logSafe("_prepareImages: Prepared ${results.whereType<Map<String, dynamic>>().length} images.");
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 {
logSafe("Opening image picker...");
if (fromCamera) {
final pickedFile = await _picker.pickImage(source: ImageSource.camera, imageQuality: 75);
if (pickedFile != null) {
selectedImages.add(File(pickedFile.path));
logSafe("Image added from camera: ${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.", );
}
}
void removeImageAt(int index) {
if (index >= 0 && index < selectedImages.length) {
logSafe("Removing image at index $index", );
selectedImages.removeAt(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);
}

View File

@ -1,248 +0,0 @@
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_Planning/daily_task_Planning_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 DailyTaskPlanningController taskController = Get.put(DailyTaskPlanningController());
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", );
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", );
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}", );
} 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,
);
}

View File

@ -1,52 +0,0 @@
import 'package:get/get.dart';
import 'package:marco/helpers/services/app_logger.dart';
import 'package:marco/helpers/services/api_service.dart';
import 'package:marco/model/attendance/organization_per_project_list_model.dart';
class OrganizationController extends GetxController {
/// List of organizations assigned to the selected project
List<Organization> organizations = [];
/// Currently selected organization (reactive)
Rxn<Organization> selectedOrganization = Rxn<Organization>();
/// Loading state for fetching organizations
final isLoadingOrganizations = false.obs;
/// Fetch organizations assigned to a given project
Future<void> fetchOrganizations(String projectId) async {
try {
isLoadingOrganizations.value = true;
final response = await ApiService.getAssignedOrganizations(projectId);
if (response != null && response.data.isNotEmpty) {
organizations = response.data;
logSafe("Organizations fetched: ${organizations.length}");
} else {
organizations = [];
logSafe("No organizations found for project $projectId",
level: LogLevel.warning);
}
} catch (e, stackTrace) {
logSafe("Failed to fetch organizations: $e",
level: LogLevel.error, error: e, stackTrace: stackTrace);
organizations = [];
} finally {
isLoadingOrganizations.value = false;
}
}
/// Select an organization
void selectOrganization(Organization? org) {
selectedOrganization.value = org;
}
/// Clear the selection (set to "All Organizations")
void clearSelection() {
selectedOrganization.value = null;
}
/// Current selection name for UI
String get currentSelection =>
selectedOrganization.value?.name ?? "All Organizations";
}

View File

@ -1,43 +0,0 @@
import 'package:get/get.dart';
import 'package:marco/helpers/services/api_service.dart';
import 'package:marco/helpers/services/app_logger.dart';
import 'package:marco/model/tenant/tenant_services_model.dart';
class ServiceController extends GetxController {
List<Service> services = [];
Service? selectedService;
final isLoadingServices = false.obs;
/// Fetch services assigned to a project
Future<void> fetchServices(String projectId) async {
try {
isLoadingServices.value = true;
final response = await ApiService.getAssignedServices(projectId);
if (response != null) {
services = response.data;
logSafe("Services fetched: ${services.length}");
} else {
logSafe("Failed to fetch services for project $projectId",
level: LogLevel.error);
}
} finally {
isLoadingServices.value = false;
update();
}
}
/// Select a service
void selectService(Service? service) {
selectedService = service;
update();
}
/// Clear selection
void clearSelection() {
selectedService = null;
update();
}
/// Current selected name
String get currentSelection => selectedService?.name ?? "All Services";
}

View File

@ -1,106 +0,0 @@
import 'package:get/get.dart';
import 'package:marco/helpers/services/app_logger.dart';
import 'package:marco/helpers/services/tenant_service.dart';
import 'package:marco/model/tenant/tenant_list_model.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart';
import 'package:marco/helpers/services/storage/local_storage.dart';
class TenantSelectionController extends GetxController {
final TenantService _tenantService = TenantService();
var tenants = <Tenant>[].obs;
var isLoading = false.obs;
@override
void onInit() {
super.onInit();
loadTenants();
}
/// Load tenants from API
Future<void> loadTenants({bool fromTenantSelectionScreen = false}) async {
try {
isLoading.value = true;
final data = await _tenantService.getTenants();
if (data != null) {
tenants.value = data.map((e) => Tenant.fromJson(e)).toList();
final recentTenantId = LocalStorage.getRecentTenantId();
// If user came from TenantSelectionScreen & recent tenant exists, auto-select
if (fromTenantSelectionScreen && recentTenantId != null) {
final tenantExists = tenants.any((t) => t.id == recentTenantId);
if (tenantExists) {
await onTenantSelected(recentTenantId);
return;
} else {
// if tenant is no longer valid, clear recentTenant
await LocalStorage.removeRecentTenantId();
}
}
// Auto-select if only one tenant
if (tenants.length == 1) {
await onTenantSelected(tenants.first.id);
}
} else {
tenants.clear();
logSafe("⚠️ No tenants found for the user.", level: LogLevel.warning);
}
} catch (e, st) {
logSafe("❌ Exception in loadTenants",
level: LogLevel.error, error: e, stackTrace: st);
} finally {
isLoading.value = false;
}
}
/// Select tenant
Future<void> onTenantSelected(String tenantId) async {
try {
isLoading.value = true;
final success = await _tenantService.selectTenant(tenantId);
if (success) {
logSafe("✅ Tenant selection successful: $tenantId");
// Store selected tenant in memory
final selectedTenant = tenants.firstWhere((t) => t.id == tenantId);
TenantService.setSelectedTenant(selectedTenant);
// 🔥 Save in LocalStorage
await LocalStorage.setRecentTenantId(tenantId);
// Navigate to dashboard
Get.offAllNamed('/dashboard');
showAppSnackbar(
title: "Success",
message: "Organization selected successfully.",
type: SnackbarType.success,
);
} else {
logSafe("❌ Tenant selection failed for: $tenantId",
level: LogLevel.warning);
// Show error snackbar
showAppSnackbar(
title: "Error",
message: "Unable to select organization. Please try again.",
type: SnackbarType.error,
);
}
} catch (e, st) {
logSafe("❌ Exception in onTenantSelected",
level: LogLevel.error, error: e, stackTrace: st);
// Show error snackbar for exception
showAppSnackbar(
title: "Error",
message: "An unexpected error occurred while selecting organization.",
type: SnackbarType.error,
);
} finally {
isLoading.value = false;
}
}
}

View File

@ -19,7 +19,6 @@ import 'package:marco/model/document/master_document_type_model.dart';
import 'package:marco/model/document/document_details_model.dart';
import 'package:marco/model/document/document_version_model.dart';
import 'package:marco/model/attendance/organization_per_project_list_model.dart';
import 'package:marco/model/tenant/tenant_services_model.dart';
class ApiService {
static const bool enableLogs = true;
@ -319,36 +318,6 @@ class ApiService {
return null;
}
//// Get Services assigned to a Project
static Future<ServiceListResponse?> getAssignedServices(
String projectId) async {
final endpoint = "${ApiEndpoints.getAssignedServices}/$projectId";
logSafe("Fetching services assigned to projectId: $projectId");
try {
final response = await _getRequest(endpoint);
if (response == null) {
logSafe("Assigned Services request failed: null response",
level: LogLevel.error);
return null;
}
final jsonResponse =
_parseResponseForAllData(response, label: "Assigned Services");
if (jsonResponse != null) {
return ServiceListResponse.fromJson(jsonResponse);
}
} catch (e, stack) {
logSafe("Exception during getAssignedServices: $e",
level: LogLevel.error);
logSafe("StackTrace: $stack", level: LogLevel.debug);
}
return null;
}
static Future<bool> postLogsApi(List<Map<String, dynamic>> logs) async {
const endpoint = "${ApiEndpoints.uploadLogs}";
logSafe("Posting logs... count=${logs.length}");

View File

@ -1,9 +1,6 @@
import 'package:get/get.dart';
import 'package:logger/logger.dart';
import 'package:marco/controller/attendance/attendance_screen_controller.dart';
import 'package:marco/controller/task_planning/daily_task_controller.dart';
import 'package:marco/controller/task_Planning/daily_task_Planning_controller.dart';
import 'package:marco/controller/expense/expense_screen_controller.dart';
import 'package:marco/controller/expense/expense_detail_controller.dart';
import 'package:marco/controller/directory/directory_controller.dart';
@ -72,26 +69,6 @@ class NotificationActionHandler {
// Call method to handle team modifications and dashboard update
_handleDashboardUpdate(data);
break;
/// 🔹 Tasks
case 'Report_Task':
_handleTaskUpdated(data, isComment: false);
_handleDashboardUpdate(data);
break;
case 'Task_Comment':
_handleTaskUpdated(data, isComment: true);
_handleDashboardUpdate(data);
break;
case 'Task_Modified':
case 'WorkArea_Modified':
case 'Floor_Modified':
case 'Building_Modified':
_handleTaskPlanningUpdated(data);
_handleDashboardUpdate(data);
break;
/// 🔹 Expenses
case 'Expenses_Modified':
_handleExpenseUpdated(data);
@ -128,23 +105,7 @@ class NotificationActionHandler {
/// ---------------------- HANDLERS ----------------------
static void _handleTaskPlanningUpdated(Map<String, dynamic> data) {
final projectId = data['ProjectId'];
if (projectId == null) {
_logger.w("⚠️ TaskPlanning update received without ProjectId: $data");
return;
}
_safeControllerUpdate<DailyTaskPlanningController>(
onFound: (controller) {
controller.fetchTaskData(projectId);
},
notFoundMessage:
'⚠️ DailyTaskPlanningController not found, cannot refresh.',
successMessage:
'✅ DailyTaskPlanningController refreshed from notification.',
);
}
static bool _isAttendanceAction(String? action) {
const validActions = {
@ -199,18 +160,6 @@ class NotificationActionHandler {
);
}
static void _handleTaskUpdated(Map<String, dynamic> data,
{required bool isComment}) {
_safeControllerUpdate<DailyTaskController>(
onFound: (controller) => controller.refreshTasksFromNotification(
projectId: data['ProjectId'],
taskAllocationId: data['TaskAllocationId'],
),
notFoundMessage: '⚠️ DailyTaskController not found, cannot update.',
successMessage: '✅ DailyTaskController refreshed from notification.',
);
}
/// ---------------------- DOCUMENT HANDLER ----------------------
static void _handleDocumentModified(Map<String, dynamic> data) {
String entityTypeId;

View File

@ -1,152 +0,0 @@
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:get/get.dart';
import 'package:marco/controller/project_controller.dart';
import 'package:marco/helpers/services/api_endpoints.dart';
import 'package:marco/helpers/services/storage/local_storage.dart';
import 'package:marco/helpers/services/app_logger.dart';
import 'package:marco/helpers/services/auth_service.dart';
import 'package:marco/model/tenant/tenant_list_model.dart';
/// Abstract interface for tenant service functionality
abstract class ITenantService {
Future<List<Map<String, dynamic>>?> getTenants({bool hasRetried = false});
Future<bool> selectTenant(String tenantId, {bool hasRetried = false});
}
/// Tenant API service
class TenantService implements ITenantService {
static const String _baseUrl = ApiEndpoints.baseUrl;
static const Map<String, String> _headers = {
'Content-Type': 'application/json',
};
/// Currently selected tenant
static Tenant? currentTenant;
/// Set the selected tenant
static void setSelectedTenant(Tenant tenant) {
currentTenant = tenant;
}
/// Check if tenant is selected
static bool get isTenantSelected => currentTenant != null;
/// Build authorized headers
static Future<Map<String, String>> _authorizedHeaders() async {
final token = await LocalStorage.getJwtToken();
if (token == null || token.isEmpty) {
throw Exception('Missing JWT token');
}
return {..._headers, 'Authorization': 'Bearer $token'};
}
/// Handle API errors
static void _handleApiError(
http.Response response, dynamic data, String context) {
final message = data['message'] ?? 'Unknown error';
final level =
response.statusCode >= 500 ? LogLevel.error : LogLevel.warning;
logSafe("$context failed: $message [Status: ${response.statusCode}]",
level: level);
}
/// Log exceptions
static void _logException(dynamic e, dynamic st, String context) {
logSafe("$context exception",
level: LogLevel.error, error: e, stackTrace: st);
}
@override
Future<List<Map<String, dynamic>>?> getTenants(
{bool hasRetried = false}) async {
try {
final headers = await _authorizedHeaders();
logSafe("➡️ GET $_baseUrl/auth/get/user/tenants\nHeaders: $headers",
level: LogLevel.info);
final response = await http
.get(Uri.parse("$_baseUrl/auth/get/user/tenants"), headers: headers);
final data = jsonDecode(response.body);
logSafe(
"⬅️ Response: ${jsonEncode(data)} [Status: ${response.statusCode}]",
level: LogLevel.info);
if (response.statusCode == 200 && data['success'] == true) {
logSafe("✅ Tenants fetched successfully.");
return List<Map<String, dynamic>>.from(data['data']);
}
if (response.statusCode == 401 && !hasRetried) {
logSafe("⚠️ Unauthorized while fetching tenants. Refreshing token...",
level: LogLevel.warning);
final refreshed = await AuthService.refreshToken();
if (refreshed) return getTenants(hasRetried: true);
logSafe("❌ Token refresh failed while fetching tenants.",
level: LogLevel.error);
return null;
}
_handleApiError(response, data, "Fetching tenants");
return null;
} catch (e, st) {
_logException(e, st, "Get Tenants API");
return null;
}
}
@override
Future<bool> selectTenant(String tenantId, {bool hasRetried = false}) async {
try {
final headers = await _authorizedHeaders();
logSafe(
"➡️ POST $_baseUrl/auth/select-tenant/$tenantId\nHeaders: $headers",
level: LogLevel.info);
final response = await http.post(
Uri.parse("$_baseUrl/auth/select-tenant/$tenantId"),
headers: headers,
);
final data = jsonDecode(response.body);
logSafe(
"⬅️ Response: ${jsonEncode(data)} [Status: ${response.statusCode}]",
level: LogLevel.info);
if (response.statusCode == 200 && data['success'] == true) {
await LocalStorage.setJwtToken(data['data']['token']);
await LocalStorage.setRefreshToken(data['data']['refreshToken']);
logSafe("✅ Tenant selected successfully. Tokens updated.");
// 🔥 Refresh projects when tenant changes
try {
final projectController = Get.find<ProjectController>();
projectController.clearProjects();
projectController.fetchProjects();
} catch (_) {
logSafe("⚠️ ProjectController not found while refreshing projects");
}
return true;
}
if (response.statusCode == 401 && !hasRetried) {
logSafe("⚠️ Unauthorized while selecting tenant. Refreshing token...",
level: LogLevel.warning);
final refreshed = await AuthService.refreshToken();
if (refreshed) return selectTenant(tenantId, hasRetried: true);
logSafe("❌ Token refresh failed while selecting tenant.",
level: LogLevel.error);
return false;
}
_handleApiError(response, data, "Selecting tenant");
return false;
} catch (e, st) {
_logException(e, st, "Select Tenant API");
return false;
}
}
}

View File

@ -1,106 +0,0 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:marco/controller/tenant/organization_selection_controller.dart';
import 'package:marco/helpers/widgets/my_text.dart';
import 'package:marco/model/attendance/organization_per_project_list_model.dart';
class OrganizationSelector extends StatelessWidget {
final OrganizationController controller;
/// Called whenever a new organization is selected (including "All Organizations").
final Future<void> Function(Organization?)? onSelectionChanged;
/// Optional height for the selector. If null, uses default padding-based height.
final double? height;
const OrganizationSelector({
super.key,
required this.controller,
this.onSelectionChanged,
this.height,
});
Widget _popupSelector({
required String currentValue,
required List<String> items,
}) {
return PopupMenuButton<String>(
color: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
onSelected: (name) async {
Organization? org = name == "All Organizations"
? null
: controller.organizations.firstWhere((e) => e.name == name);
controller.selectOrganization(org);
if (onSelectionChanged != null) {
await onSelectionChanged!(org);
}
},
itemBuilder: (context) => items
.map((e) => PopupMenuItem<String>(value: e, child: MyText(e)))
.toList(),
child: Container(
height: height,
padding: const EdgeInsets.symmetric(horizontal: 12),
decoration: BoxDecoration(
color: Colors.white,
border: Border.all(color: Colors.grey.shade300),
borderRadius: BorderRadius.circular(12),
),
child: Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
child: Text(
currentValue,
style: const TextStyle(
color: Colors.black87,
fontSize: 13,
height: 1.2,
),
overflow: TextOverflow.ellipsis,
),
),
const Icon(Icons.arrow_drop_down, color: Colors.grey, size: 18),
],
),
),
),
);
}
@override
Widget build(BuildContext context) {
return Obx(() {
if (controller.isLoadingOrganizations.value) {
return const Center(child: CircularProgressIndicator());
} else if (controller.organizations.isEmpty) {
return Center(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0),
child: MyText.bodyMedium(
"No organizations found",
fontWeight: 500,
color: Colors.grey,
),
),
);
}
final orgNames = [
"All Organizations",
...controller.organizations.map((e) => e.name)
];
// Listen to selectedOrganization.value
return _popupSelector(
currentValue: controller.currentSelection,
items: orgNames,
);
});
}
}

View File

@ -1,114 +0,0 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:marco/helpers/widgets/my_text.dart';
import 'package:marco/model/tenant/tenant_services_model.dart';
import 'package:marco/controller/tenant/service_controller.dart';
class ServiceSelector extends StatelessWidget {
final ServiceController controller;
/// Called whenever a new service is selected (including "All Services")
final Future<void> Function(Service?)? onSelectionChanged;
/// Optional height for the selector
final double? height;
const ServiceSelector({
super.key,
required this.controller,
this.onSelectionChanged,
this.height,
});
Widget _popupSelector({
required String currentValue,
required List<String> items,
}) {
return PopupMenuButton<String>(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
onSelected: items.isEmpty
? null
: (name) async {
Service? service = name == "All Services"
? null
: controller.services.firstWhere((e) => e.name == name);
controller.selectService(service);
if (onSelectionChanged != null) {
await onSelectionChanged!(service);
}
},
itemBuilder: (context) {
if (items.isEmpty || items.length == 1 && items[0] == "All Services") {
return [
const PopupMenuItem<String>(
enabled: false,
child: Center(
child: Text(
"No services found",
style: TextStyle(color: Colors.grey),
),
),
),
];
}
return items
.map((e) => PopupMenuItem<String>(value: e, child: MyText(e)))
.toList();
},
child: Container(
height: height,
padding: const EdgeInsets.symmetric(horizontal: 12),
decoration: BoxDecoration(
color: Colors.grey.shade100,
border: Border.all(color: Colors.grey.shade300),
borderRadius: BorderRadius.circular(10),
),
child: Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
currentValue.isEmpty ? "No services found" : currentValue,
style: const TextStyle(
color: Colors.black87,
fontSize: 13,
height: 1.2,
),
overflow: TextOverflow.ellipsis,
),
),
const Icon(Icons.arrow_drop_down, color: Colors.grey, size: 18),
],
),
),
),
);
}
@override
Widget build(BuildContext context) {
return Obx(() {
if (controller.isLoadingServices.value) {
return const Center(child: CircularProgressIndicator());
}
final serviceNames = controller.services.isEmpty
? <String>[]
: <String>[
"All Services",
...controller.services.map((e) => e.name).toList(),
];
final currentValue =
controller.services.isEmpty ? "" : controller.currentSelection;
return _popupSelector(
currentValue: currentValue,
items: serviceNames,
);
});
}
}

View File

@ -1,368 +0,0 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:marco/controller/task_Planning/daily_task_Planning_controller.dart';
import 'package:marco/controller/project_controller.dart';
import 'package:marco/helpers/widgets/my_spacing.dart';
import 'package:marco/helpers/widgets/my_text.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart';
import 'package:marco/helpers/utils/base_bottom_sheet.dart';
class AssignTaskBottomSheet extends StatefulWidget {
final String workLocation;
final String activityName;
final int pendingTask;
final String workItemId;
final DateTime assignmentDate;
final String buildingName;
final String floorName;
final String workAreaName;
const AssignTaskBottomSheet({
super.key,
required this.buildingName,
required this.workLocation,
required this.floorName,
required this.workAreaName,
required this.activityName,
required this.pendingTask,
required this.workItemId,
required this.assignmentDate,
});
@override
State<AssignTaskBottomSheet> createState() => _AssignTaskBottomSheetState();
}
class _AssignTaskBottomSheetState extends State<AssignTaskBottomSheet> {
final DailyTaskPlanningController controller = Get.find();
final ProjectController projectController = Get.find();
final TextEditingController targetController = TextEditingController();
final TextEditingController descriptionController = TextEditingController();
final ScrollController _employeeListScrollController = ScrollController();
String? selectedProjectId;
@override
void initState() {
super.initState();
selectedProjectId = projectController.selectedProjectId.value;
WidgetsBinding.instance.addPostFrameCallback((_) {
if (selectedProjectId != null) {
controller.fetchEmployeesByProject(selectedProjectId!);
}
});
}
@override
void dispose() {
_employeeListScrollController.dispose();
targetController.dispose();
descriptionController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Obx(() => BaseBottomSheet(
title: "Assign Task",
child: _buildAssignTaskForm(),
onCancel: () => Get.back(),
onSubmit: _onAssignTaskPressed,
isSubmitting: controller.isAssigningTask.value,
));
}
Widget _buildAssignTaskForm() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_infoRow(Icons.location_on, "Work Location",
"${widget.buildingName} > ${widget.floorName} > ${widget.workAreaName} > ${widget.activityName}"),
Divider(),
_infoRow(Icons.pending_actions, "Pending Task of Activity",
"${widget.pendingTask}"),
Divider(),
GestureDetector(
onTap: _onRoleMenuPressed,
child: Row(
children: [
MyText.titleMedium("Select Team :", fontWeight: 600),
const SizedBox(width: 4),
const Icon(Icons.tune, color: Color.fromARGB(255, 95, 132, 255)),
],
),
),
MySpacing.height(8),
Container(
constraints: const BoxConstraints(maxHeight: 150),
child: _buildEmployeeList(),
),
MySpacing.height(8),
_buildSelectedEmployees(),
_buildTextField(
icon: Icons.track_changes,
label: "Target for Today :",
controller: targetController,
hintText: "Enter target",
keyboardType: TextInputType.number,
validatorType: "target",
),
MySpacing.height(24),
_buildTextField(
icon: Icons.description,
label: "Description :",
controller: descriptionController,
hintText: "Enter task description",
maxLines: 3,
validatorType: "description",
),
],
);
}
void _onRoleMenuPressed() {
final RenderBox overlay =
Overlay.of(context).context.findRenderObject() as RenderBox;
final Size screenSize = overlay.size;
showMenu(
context: context,
position: RelativeRect.fromLTRB(
screenSize.width / 2 - 100,
screenSize.height / 2 - 20,
screenSize.width / 2 - 100,
screenSize.height / 2 - 20,
),
items: [
const PopupMenuItem(value: 'all', child: Text("All Roles")),
...controller.roles.map((role) {
return PopupMenuItem(
value: role['id'].toString(),
child: Text(role['name'] ?? 'Unknown Role'),
);
}),
],
).then((value) {
if (value != null) {
controller.onRoleSelected(value == 'all' ? null : value);
}
});
}
Widget _buildEmployeeList() {
return Obx(() {
if (controller.isLoading.value) {
return const Center(child: CircularProgressIndicator());
}
final selectedRoleId = controller.selectedRoleId.value;
final filteredEmployees = selectedRoleId == null
? controller.employees
: controller.employees
.where((e) => e.jobRoleID.toString() == selectedRoleId)
.toList();
if (filteredEmployees.isEmpty) {
return const Text("No employees found for selected role.");
}
return Scrollbar(
controller: _employeeListScrollController,
thumbVisibility: true,
child: ListView.builder(
controller: _employeeListScrollController,
shrinkWrap: true,
itemCount: filteredEmployees.length,
itemBuilder: (context, index) {
final employee = filteredEmployees[index];
final rxBool = controller.uploadingStates[employee.id];
return Obx(() => Padding(
padding: const EdgeInsets.symmetric(vertical: 2),
child: Row(
children: [
Checkbox(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(4),
),
value: rxBool?.value ?? false,
onChanged: (bool? selected) {
if (rxBool != null) {
rxBool.value = selected ?? false;
controller.updateSelectedEmployees();
}
},
fillColor:
WidgetStateProperty.resolveWith<Color>((states) {
if (states.contains(WidgetState.selected)) {
return const Color.fromARGB(255, 95, 132, 255);
}
return Colors.transparent;
}),
checkColor: Colors.white,
side: const BorderSide(color: Colors.black),
),
const SizedBox(width: 8),
Expanded(
child: Text(employee.name,
style: const TextStyle(fontSize: 14))),
],
),
));
},
),
);
});
}
Widget _buildSelectedEmployees() {
return Obx(() {
if (controller.selectedEmployees.isEmpty) return Container();
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Wrap(
spacing: 4,
runSpacing: 4,
children: controller.selectedEmployees.map((e) {
return Obx(() {
final isSelected =
controller.uploadingStates[e.id]?.value ?? false;
if (!isSelected) return Container();
return Chip(
label:
Text(e.name, style: const TextStyle(color: Colors.white)),
backgroundColor: const Color.fromARGB(255, 95, 132, 255),
deleteIcon: const Icon(Icons.close, color: Colors.white),
onDeleted: () {
controller.uploadingStates[e.id]?.value = false;
controller.updateSelectedEmployees();
},
);
});
}).toList(),
),
);
});
}
Widget _buildTextField({
required IconData icon,
required String label,
required TextEditingController controller,
required String hintText,
TextInputType keyboardType = TextInputType.text,
int maxLines = 1,
required String validatorType,
}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(icon, size: 18, color: Colors.black54),
const SizedBox(width: 6),
MyText.titleMedium(label, fontWeight: 600),
],
),
MySpacing.height(6),
TextFormField(
controller: controller,
keyboardType: keyboardType,
maxLines: maxLines,
decoration: const InputDecoration(
hintText: '',
border: OutlineInputBorder(),
),
validator: (value) =>
this.controller.formFieldValidator(value, fieldType: validatorType),
),
],
);
}
Widget _infoRow(IconData icon, String title, String value) {
return Padding(
padding: MySpacing.y(6),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(icon, size: 20, color: Colors.grey[700]),
const SizedBox(width: 8),
Expanded(
child: RichText(
text: TextSpan(
children: [
WidgetSpan(
child: MyText.titleMedium("$title: ",
fontWeight: 600, color: Colors.black),
),
TextSpan(
text: value,
style: const TextStyle(color: Colors.black),
),
],
),
),
),
],
),
);
}
void _onAssignTaskPressed() {
final selectedTeam = controller.uploadingStates.entries
.where((e) => e.value.value)
.map((e) => e.key)
.toList();
if (selectedTeam.isEmpty) {
showAppSnackbar(
title: "Team Required",
message: "Please select at least one team member",
type: SnackbarType.error,
);
return;
}
final target = int.tryParse(targetController.text.trim());
if (target == null || target <= 0) {
showAppSnackbar(
title: "Invalid Input",
message: "Please enter a valid target number",
type: SnackbarType.error,
);
return;
}
if (target > widget.pendingTask) {
showAppSnackbar(
title: "Target Too High",
message:
"Target cannot be greater than pending task (${widget.pendingTask})",
type: SnackbarType.error,
);
return;
}
final description = descriptionController.text.trim();
if (description.isEmpty) {
showAppSnackbar(
title: "Description Required",
message: "Please enter a description",
type: SnackbarType.error,
);
return;
}
controller.assignDailyTask(
workItemId: widget.workItemId,
plannedTask: target,
description: description,
taskTeam: selectedTeam,
assignmentDate: widget.assignmentDate,
);
}
}

View File

@ -1,678 +0,0 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:intl/intl.dart';
import 'dart:io';
import 'dart:math' as math;
// --- Assumed Imports (ensure these paths are correct in your project) ---
import 'package:marco/controller/task_planning/report_task_controller.dart';
import 'package:marco/helpers/utils/mixins/ui_mixin.dart';
import 'package:marco/helpers/widgets/my_button.dart';
import 'package:marco/helpers/widgets/my_spacing.dart';
import 'package:marco/helpers/widgets/my_text.dart';
import 'package:marco/helpers/widgets/my_text_style.dart';
import 'package:marco/helpers/widgets/avatar.dart';
import 'package:marco/helpers/widgets/my_team_model_sheet.dart';
import 'package:marco/helpers/widgets/image_viewer_dialog.dart';
import 'package:marco/model/dailyTaskPlanning/create_task_botom_sheet.dart';
import 'package:marco/helpers/utils/base_bottom_sheet.dart';
// --- Form Field Keys (Unchanged) ---
class _FormFieldKeys {
static const String assignedDate = 'assigned_date';
static const String assignedBy = 'assigned_by';
static const String workArea = 'work_area';
static const String activity = 'activity';
static const String plannedWork = 'planned_work';
static const String completedWork = 'completed_work';
static const String teamMembers = 'team_members';
static const String assigned = 'assigned';
static const String taskId = 'task_id';
static const String comment = 'comment';
}
// --- Main Widget: CommentTaskBottomSheet ---
class CommentTaskBottomSheet extends StatefulWidget {
final Map<String, dynamic> taskData;
final VoidCallback? onCommentSuccess;
final String taskDataId;
final String workAreaId;
final String activityId;
const CommentTaskBottomSheet({
super.key,
required this.taskData,
this.onCommentSuccess,
required this.taskDataId,
required this.workAreaId,
required this.activityId,
});
@override
State<CommentTaskBottomSheet> createState() => _CommentTaskBottomSheetState();
}
class _Member {
final String firstName;
_Member(this.firstName);
}
class _CommentTaskBottomSheetState extends State<CommentTaskBottomSheet>
with UIMixin {
late final ReportTaskController controller;
List<Map<String, dynamic>> _sortedComments = [];
@override
void initState() {
super.initState();
controller = Get.put(ReportTaskController(),
tag: widget.taskData['taskId'] ?? UniqueKey().toString());
_initializeControllerData();
final comments = List<Map<String, dynamic>>.from(
widget.taskData['taskComments'] as List? ?? []);
comments.sort((a, b) {
final aDate = DateTime.tryParse(a['date'] ?? '') ??
DateTime.fromMillisecondsSinceEpoch(0);
final bDate = DateTime.tryParse(b['date'] ?? '') ??
DateTime.fromMillisecondsSinceEpoch(0);
return bDate.compareTo(aDate); // Newest first
});
_sortedComments = comments;
}
void _initializeControllerData() {
final data = widget.taskData;
final fieldMappings = {
_FormFieldKeys.assignedDate: data['assignedOn'],
_FormFieldKeys.assignedBy: data['assignedBy'],
_FormFieldKeys.workArea: data['location'],
_FormFieldKeys.activity: data['activity'],
_FormFieldKeys.plannedWork: data['plannedWork'],
_FormFieldKeys.completedWork: data['completedWork'],
_FormFieldKeys.teamMembers: (data['teamMembers'] as List?)?.join(', '),
_FormFieldKeys.assigned: data['assigned'],
_FormFieldKeys.taskId: data['taskId'],
};
for (final entry in fieldMappings.entries) {
controller.basicValidator.getController(entry.key)?.text =
entry.value ?? '';
}
controller.basicValidator.getController(_FormFieldKeys.comment)?.clear();
controller.selectedImages.clear();
}
String _timeAgo(String dateString) {
// This logic remains unchanged
try {
final date = DateTime.parse(dateString + "Z").toLocal();
final difference = DateTime.now().difference(date);
if (difference.inDays > 8) return DateFormat('dd-MM-yyyy').format(date);
if (difference.inDays >= 1)
return '${difference.inDays} day${difference.inDays > 1 ? 's' : ''} ago';
if (difference.inHours >= 1)
return '${difference.inHours} hr${difference.inHours > 1 ? 's' : ''} ago';
if (difference.inMinutes >= 1)
return '${difference.inMinutes} min${difference.inMinutes > 1 ? 's' : ''} ago';
return 'just now';
} catch (e) {
debugPrint('Error parsing date for timeAgo: $e');
return dateString;
}
}
@override
Widget build(BuildContext context) {
// --- REFACTORING POINT ---
// The entire widget now returns a BaseBottomSheet, passing the content as its child.
// The GetBuilder provides reactive state (like isLoading) to the BaseBottomSheet.
return GetBuilder<ReportTaskController>(
tag: widget.taskData['taskId'] ?? '',
builder: (controller) {
return BaseBottomSheet(
title: "Task Details & Comments",
onCancel: () => Navigator.of(context).pop(),
onSubmit: _submitComment,
isSubmitting: controller.isLoading.value,
bottomContent: _buildCommentsSection(),
child: Form(
// moved to last
key: controller.basicValidator.formKey,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildHeaderActions(),
MySpacing.height(12),
_buildTaskDetails(),
_buildReportedImages(),
_buildCommentInput(),
_buildImagePicker(),
],
),
),
);
},
);
}
// --- REFACTORING POINT ---
// The original _buildHeader is now split. The title is handled by BaseBottomSheet.
// This new widget contains the remaining actions from the header.
Widget _buildHeaderActions() {
return Align(
alignment: Alignment.centerRight,
child: InkWell(
onTap: () => _showCreateTaskBottomSheet(),
borderRadius: BorderRadius.circular(16),
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: Colors.blueAccent.withOpacity(0.1),
borderRadius: BorderRadius.circular(16),
),
child: MyText.bodySmall(
"+ Create Task",
fontWeight: 600,
color: Colors.blueAccent,
),
),
),
);
}
Widget _buildTaskDetails() {
return Column(
children: [
_buildDetailRow(
"Assigned By",
controller.basicValidator
.getController(_FormFieldKeys.assignedBy)
?.text,
icon: Icons.person_outline),
_buildDetailRow(
"Work Area",
controller.basicValidator
.getController(_FormFieldKeys.workArea)
?.text,
icon: Icons.place_outlined),
_buildDetailRow(
"Activity",
controller.basicValidator
.getController(_FormFieldKeys.activity)
?.text,
icon: Icons.assignment_outlined),
_buildDetailRow(
"Planned Work",
controller.basicValidator
.getController(_FormFieldKeys.plannedWork)
?.text,
icon: Icons.schedule_outlined),
_buildDetailRow(
"Completed Work",
controller.basicValidator
.getController(_FormFieldKeys.completedWork)
?.text,
icon: Icons.done_all_outlined),
_buildTeamMembers(),
],
);
}
Widget _buildReportedImages() {
final imageUrls =
List<String>.from(widget.taskData['reportedPreSignedUrls'] ?? []);
if (imageUrls.isEmpty) return const SizedBox.shrink();
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: _buildSectionHeader("Reported Images", Icons.image_outlined),
),
// --- Refactoring Note ---
// Using the reusable _ImageHorizontalListView widget.
_ImageHorizontalListView(
imageSources: imageUrls,
onPreview: (index) => _showImageViewer(imageUrls, index),
),
MySpacing.height(16),
],
);
}
Widget _buildCommentInput() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildSectionHeader("Add Note", Icons.comment_outlined),
MySpacing.height(8),
TextFormField(
validator:
controller.basicValidator.getValidation(_FormFieldKeys.comment),
controller:
controller.basicValidator.getController(_FormFieldKeys.comment),
keyboardType: TextInputType.multiline,
maxLines: null, // Allows for multiline input
decoration: InputDecoration(
hintText: "eg: Work done successfully",
hintStyle: MyTextStyle.bodySmall(xMuted: true),
border: outlineInputBorder,
enabledBorder: outlineInputBorder,
focusedBorder: focusedInputBorder,
contentPadding: MySpacing.all(16),
isCollapsed: true,
floatingLabelBehavior: FloatingLabelBehavior.never,
),
),
MySpacing.height(16),
],
);
}
Widget _buildImagePicker() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildSectionHeader("Attach Photos", Icons.camera_alt_outlined),
MySpacing.height(12),
Obx(() {
final images = controller.selectedImages;
return Column(
children: [
// --- Refactoring Note ---
// Using the reusable _ImageHorizontalListView for picked images.
_ImageHorizontalListView(
imageSources: images.toList(),
onPreview: (index) => _showImageViewer(images.toList(), index),
onRemove: (index) => controller.removeImageAt(index),
emptyStatePlaceholder: Container(
height: 70,
width: double.infinity,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.grey.shade300, width: 1.5),
color: Colors.grey.shade100,
),
child: Center(
child: Icon(Icons.photo_library_outlined,
size: 36, color: Colors.grey.shade400),
),
),
),
MySpacing.height(16),
Row(
children: [
_buildPickerButton(
onTap: () => controller.pickImages(fromCamera: true),
icon: Icons.camera_alt,
label: 'Capture',
),
MySpacing.width(12),
_buildPickerButton(
onTap: () => controller.pickImages(fromCamera: false),
icon: Icons.upload_file,
label: 'Upload',
),
],
),
],
);
}),
],
);
}
Widget _buildCommentsSection() {
if (_sortedComments.isEmpty) return const SizedBox.shrink();
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MySpacing.height(24),
_buildSectionHeader("Comments", Icons.chat_bubble_outline),
MySpacing.height(12),
// --- Refactoring Note ---
// Using a ListView instead of a fixed-height SizedBox for better responsiveness.
// It's constrained by the parent SingleChildScrollView.
ListView.builder(
shrinkWrap:
true, // Important for ListView inside SingleChildScrollView
physics:
const NeverScrollableScrollPhysics(), // Parent handles scrolling
itemCount: _sortedComments.length,
itemBuilder: (context, index) {
final comment = _sortedComments[index];
// --- Refactoring Note ---
// Extracted the comment item into its own widget for clarity.
return _CommentCard(
comment: comment,
timeAgo: _timeAgo(comment['date'] ?? ''),
onPreviewImage: (imageUrls, idx) =>
_showImageViewer(imageUrls, idx),
);
},
),
],
);
}
// --- Helper and Builder methods ---
Widget _buildDetailRow(String label, String? value,
{required IconData icon}) {
return Padding(
padding: const EdgeInsets.only(bottom: 16),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(right: 8.0, top: 2),
child: Icon(icon, size: 18, color: Colors.grey[700]),
),
MyText.titleSmall("$label:", fontWeight: 600),
MySpacing.width(12),
Expanded(
child: MyText.bodyMedium(
value != null && value.isNotEmpty ? value : "-",
color: Colors.black87,
),
),
],
),
);
}
Widget _buildSectionHeader(String title, IconData icon) {
return Row(
children: [
Icon(icon, size: 18, color: Colors.grey[700]),
MySpacing.width(8),
MyText.titleSmall(title, fontWeight: 600),
],
);
}
Widget _buildTeamMembers() {
final teamMembersText = controller.basicValidator
.getController(_FormFieldKeys.teamMembers)
?.text ??
'';
final members = teamMembersText
.split(',')
.map((e) => e.trim())
.where((e) => e.isNotEmpty)
.toList();
if (members.isEmpty) return const SizedBox.shrink();
const double avatarSize = 32.0;
const double avatarOverlap = 22.0;
return Padding(
padding: const EdgeInsets.only(bottom: 16.0),
child: Row(
children: [
Icon(Icons.group_outlined, size: 18, color: Colors.grey[700]),
MySpacing.width(8),
MyText.titleSmall("Team:", fontWeight: 600),
MySpacing.width(12),
GestureDetector(
onTap: () => TeamBottomSheet.show(
context: context,
teamMembers: members.map((name) => _Member(name)).toList()),
child: SizedBox(
height: avatarSize,
// Calculate width based on number of avatars shown
width: (math.min(members.length, 3) * avatarOverlap) +
(avatarSize - avatarOverlap),
child: Stack(
children: [
...List.generate(math.min(members.length, 3), (i) {
return Positioned(
left: i * avatarOverlap,
child: Tooltip(
message: members[i],
child: Avatar(
firstName: members[i],
lastName: '',
size: avatarSize),
),
);
}),
if (members.length > 3)
Positioned(
left: 3 * avatarOverlap,
child: CircleAvatar(
radius: avatarSize / 2,
backgroundColor: Colors.grey.shade300,
child: MyText.bodySmall('+${members.length - 3}',
fontWeight: 600),
),
),
],
),
),
),
],
),
);
}
Widget _buildPickerButton(
{required VoidCallback onTap,
required IconData icon,
required String label}) {
return Expanded(
child: MyButton.outlined(
onPressed: onTap,
padding: MySpacing.xy(12, 10),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(icon, size: 18, color: Colors.blueAccent),
MySpacing.width(8),
MyText.bodySmall(label, color: Colors.blueAccent, fontWeight: 600),
],
),
),
);
}
// --- Action Handlers ---
void _showCreateTaskBottomSheet() {
showCreateTaskBottomSheet(
workArea: widget.taskData['location'] ?? '',
activity: widget.taskData['activity'] ?? '',
completedWork: widget.taskData['completedWork'] ?? '',
unit: widget.taskData['unit'] ?? '',
onCategoryChanged: (category) =>
debugPrint("Category changed to: $category"),
parentTaskId: widget.taskDataId,
plannedTask: int.tryParse(widget.taskData['plannedWork'] ?? '0') ?? 0,
activityId: widget.activityId,
workAreaId: widget.workAreaId,
onSubmit: () => Navigator.of(context).pop(),
);
}
void _showImageViewer(List<dynamic> sources, int initialIndex) {
showDialog(
context: context,
barrierColor: Colors.black87,
builder: (_) => ImageViewerDialog(
imageSources: sources,
initialIndex: initialIndex,
),
);
}
Future<void> _submitComment() async {
if (controller.basicValidator.validateForm()) {
await controller.commentTask(
projectId: controller.basicValidator
.getController(_FormFieldKeys.taskId)
?.text ??
'',
comment: controller.basicValidator
.getController(_FormFieldKeys.comment)
?.text ??
'',
images: controller.selectedImages,
);
// Callback to the parent widget to refresh data if needed
widget.onCommentSuccess?.call();
}
}
}
// --- Refactoring Note ---
// A reusable widget for displaying a horizontal list of images.
// It can handle both network URLs (String) and local files (File).
class _ImageHorizontalListView extends StatelessWidget {
final List<dynamic> imageSources; // Can be List<String> or List<File>
final Function(int) onPreview;
final Function(int)? onRemove;
final Widget? emptyStatePlaceholder;
const _ImageHorizontalListView({
required this.imageSources,
required this.onPreview,
this.onRemove,
this.emptyStatePlaceholder,
});
@override
Widget build(BuildContext context) {
if (imageSources.isEmpty) {
return emptyStatePlaceholder ?? const SizedBox.shrink();
}
return SizedBox(
height: 70,
child: ListView.separated(
scrollDirection: Axis.horizontal,
itemCount: imageSources.length,
separatorBuilder: (_, __) => const SizedBox(width: 12),
itemBuilder: (context, index) {
final source = imageSources[index];
return GestureDetector(
onTap: () => onPreview(index),
child: Stack(
clipBehavior: Clip.none,
children: [
ClipRRect(
borderRadius: BorderRadius.circular(12),
child: source is File
? Image.file(source,
width: 70, height: 70, fit: BoxFit.cover)
: Image.network(
source as String,
width: 70,
height: 70,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) =>
Container(
width: 70,
height: 70,
color: Colors.grey.shade200,
child: Icon(Icons.broken_image,
color: Colors.grey[600]),
),
),
),
if (onRemove != null)
Positioned(
top: -6,
right: -6,
child: GestureDetector(
onTap: () => onRemove!(index),
child: Container(
padding: const EdgeInsets.all(2),
decoration: const BoxDecoration(
color: Colors.red, shape: BoxShape.circle),
child: const Icon(Icons.close,
size: 16, color: Colors.white),
),
),
),
],
),
);
},
),
);
}
}
// --- Refactoring Note ---
// A dedicated widget for a single comment card. This cleans up the main
// widget's build method and makes the comment layout easier to manage.
class _CommentCard extends StatelessWidget {
final Map<String, dynamic> comment;
final String timeAgo;
final Function(List<String> imageUrls, int index) onPreviewImage;
const _CommentCard({
required this.comment,
required this.timeAgo,
required this.onPreviewImage,
});
@override
Widget build(BuildContext context) {
final commentedBy = comment['commentedBy'] ?? 'Unknown';
final commentText = comment['text'] ?? '-';
final imageUrls = List<String>.from(comment['preSignedUrls'] ?? []);
return Container(
margin: const EdgeInsets.symmetric(vertical: 6),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.grey.shade200)),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Avatar(
firstName: commentedBy.split(' ').first,
lastName: commentedBy.split(' ').length > 1
? commentedBy.split(' ').last
: '',
size: 32,
),
MySpacing.width(12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MyText.bodyMedium(commentedBy,
fontWeight: 700, color: Colors.black87),
MyText.bodySmall(timeAgo,
color: Colors.black54, fontSize: 12),
],
),
),
],
),
MySpacing.height(12),
MyText.bodyMedium(commentText, color: Colors.black87),
if (imageUrls.isNotEmpty) ...[
MySpacing.height(12),
_ImageHorizontalListView(
imageSources: imageUrls,
onPreview: (index) => onPreviewImage(imageUrls, index),
),
],
],
),
);
}
}

View File

@ -1,213 +0,0 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:marco/controller/task_planning/add_task_controller.dart';
import 'package:marco/helpers/widgets/my_text.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart';
import 'package:marco/helpers/utils/base_bottom_sheet.dart';
void showCreateTaskBottomSheet({
required String workArea,
required String activity,
required String completedWork,
required String unit,
required Function(String) onCategoryChanged,
required String parentTaskId,
required int plannedTask,
required String activityId,
required String workAreaId,
required VoidCallback onSubmit,
}) {
final controller = Get.put(AddTaskController());
final TextEditingController plannedTaskController =
TextEditingController(text: plannedTask.toString());
final TextEditingController descriptionController = TextEditingController();
Get.bottomSheet(
StatefulBuilder(
builder: (context, setState) {
return BaseBottomSheet(
title: "Create Task",
onCancel: () => Get.back(),
onSubmit: () async {
final plannedValue =
int.tryParse(plannedTaskController.text.trim()) ?? 0;
final comment = descriptionController.text.trim();
final selectedCategoryId = controller.selectedCategoryId.value;
if (selectedCategoryId == null) {
showAppSnackbar(
title: "error",
message: "Please select a work category!",
type: SnackbarType.error,
);
return;
}
final success = await controller.createTask(
parentTaskId: parentTaskId,
plannedTask: plannedValue,
comment: comment,
workAreaId: workAreaId,
activityId: activityId,
categoryId: selectedCategoryId,
);
if (success) {
Get.back();
Future.delayed(const Duration(milliseconds: 300), () {
onSubmit();
showAppSnackbar(
title: "Success",
message: "Task created successfully!",
type: SnackbarType.success,
);
});
}
},
submitText: "Submit",
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_infoCardSection([
_infoRowWithIcon(
Icons.workspaces, "Selected Work Area", workArea),
_infoRowWithIcon(Icons.list_alt, "Selected Activity", activity),
_infoRowWithIcon(Icons.check_circle_outline, "Completed Work",
completedWork),
]),
const SizedBox(height: 16),
_sectionTitle(Icons.edit_calendar, "Planned Work"),
const SizedBox(height: 6),
_customTextField(
controller: plannedTaskController,
hint: "Enter planned work",
keyboardType: TextInputType.number,
),
const SizedBox(height: 16),
_sectionTitle(Icons.description_outlined, "Comment"),
const SizedBox(height: 6),
_customTextField(
controller: descriptionController,
hint: "Enter task description",
maxLines: 3,
),
const SizedBox(height: 16),
_sectionTitle(Icons.category_outlined, "Selected Work Category"),
const SizedBox(height: 6),
Obx(() {
final categoryMap = controller.categoryIdNameMap;
final String selectedName =
controller.selectedCategoryId.value != null
? (categoryMap[controller.selectedCategoryId.value!] ??
'Select Category')
: 'Select Category';
return Container(
width: double.infinity,
padding:
const EdgeInsets.symmetric(horizontal: 12, vertical: 14),
decoration: BoxDecoration(
color: Colors.grey.shade100,
border: Border.all(color: Colors.grey.shade300),
borderRadius: BorderRadius.circular(8),
),
child: PopupMenuButton<String>(
padding: EdgeInsets.zero,
onSelected: (val) {
controller.selectCategory(val);
onCategoryChanged(val);
},
itemBuilder: (context) => categoryMap.entries
.map((entry) => PopupMenuItem<String>(
value: entry.key,
child: Text(entry.value),
))
.toList(),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
selectedName,
style: const TextStyle(
fontSize: 14, color: Colors.black87),
),
const Icon(Icons.arrow_drop_down),
],
),
),
);
}),
],
),
);
},
),
isScrollControlled: true,
);
}
Widget _sectionTitle(IconData icon, String title) {
return Row(
children: [
Icon(icon, color: Colors.grey[700], size: 18),
const SizedBox(width: 8),
MyText.bodyMedium(title, fontWeight: 600),
],
);
}
Widget _customTextField({
required TextEditingController controller,
required String hint,
int maxLines = 1,
TextInputType keyboardType = TextInputType.text,
}) {
return TextField(
controller: controller,
maxLines: maxLines,
keyboardType: keyboardType,
decoration: InputDecoration(
hintText: hint,
filled: true,
fillColor: Colors.grey.shade100,
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
),
);
}
Widget _infoCardSection(List<Widget> children) {
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.grey.shade300),
),
child: Column(children: children),
);
}
Widget _infoRowWithIcon(IconData icon, String title, String value) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 6),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(icon, color: Colors.grey[700], size: 18),
const SizedBox(width: 8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MyText.bodyMedium(title, fontWeight: 600),
const SizedBox(height: 2),
MyText.bodySmall(value, color: Colors.grey[800]),
],
),
),
],
),
);
}

View File

@ -1,83 +0,0 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:marco/controller/task_planning/daily_task_controller.dart';
import 'package:marco/controller/permission_controller.dart';
import 'package:marco/helpers/utils/base_bottom_sheet.dart';
import 'package:marco/helpers/widgets/my_text.dart';
class DailyProgressReportFilter extends StatelessWidget {
final DailyTaskController controller;
final PermissionController permissionController;
const DailyProgressReportFilter({
super.key,
required this.controller,
required this.permissionController,
});
String getLabelText() {
final startDate = controller.startDateTask;
final endDate = controller.endDateTask;
if (startDate != null && endDate != null) {
final start = DateFormat('dd MM yyyy').format(startDate);
final end = DateFormat('dd MM yyyy').format(endDate);
return "$start - $end";
}
return "Select Date Range";
}
@override
Widget build(BuildContext context) {
return BaseBottomSheet(
title: "Filter Tasks",
onCancel: () => Navigator.pop(context),
onSubmit: () {
Navigator.pop(context, {
'startDate': controller.startDateTask,
'endDate': controller.endDateTask,
});
},
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MyText.titleSmall("Select Date Range", fontWeight: 600),
const SizedBox(height: 8),
InkWell(
borderRadius: BorderRadius.circular(10),
onTap: () => controller.selectDateRangeForTaskData(
context,
controller,
),
child: Ink(
decoration: BoxDecoration(
color: Colors.grey.shade100,
border: Border.all(color: Colors.grey.shade400),
borderRadius: BorderRadius.circular(10),
),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
child: Row(
children: [
Icon(Icons.date_range, color: Colors.blue.shade600),
const SizedBox(width: 12),
Expanded(
child: Text(
getLabelText(),
style: const TextStyle(
fontSize: 16,
color: Colors.black87,
fontWeight: FontWeight.w500,
),
overflow: TextOverflow.ellipsis,
),
),
const Icon(Icons.arrow_drop_down, color: Colors.grey),
],
),
),
),
],
),
);
}
}

View File

@ -1,222 +0,0 @@
class TaskModel {
final DateTime assignmentDate;
final DateTime? reportedDate;
final String id;
final WorkItem? workItem;
final String workItemId;
final double plannedTask;
final double completedTask;
final AssignedBy assignedBy;
final AssignedBy? approvedBy;
final List<TeamMember> teamMembers;
final List<Comment> comments;
final List<String> reportedPreSignedUrls;
TaskModel({
required this.assignmentDate,
this.reportedDate,
required this.id,
required this.workItem,
required this.workItemId,
required this.plannedTask,
required this.completedTask,
required this.assignedBy,
this.approvedBy,
required this.teamMembers,
required this.comments,
required this.reportedPreSignedUrls,
});
factory TaskModel.fromJson(Map<String, dynamic> json) {
return TaskModel(
assignmentDate: DateTime.parse(json['assignmentDate']),
reportedDate: json['reportedDate'] != null
? DateTime.tryParse(json['reportedDate'])
: null,
id: json['id'],
workItem:
json['workItem'] != null ? WorkItem.fromJson(json['workItem']) : null,
workItemId: json['workItemId'],
plannedTask: (json['plannedTask'] as num).toDouble(),
completedTask: (json['completedTask'] as num).toDouble(),
assignedBy: AssignedBy.fromJson(json['assignedBy']),
approvedBy: json['approvedBy'] != null
? AssignedBy.fromJson(json['approvedBy'])
: null,
teamMembers: (json['teamMembers'] as List)
.map((e) => TeamMember.fromJson(e))
.toList(),
comments:
(json['comments'] as List).map((e) => Comment.fromJson(e)).toList(),
reportedPreSignedUrls: (json['reportedPreSignedUrls'] as List<dynamic>?)
?.map((e) => e.toString())
.toList() ??
[],
);
}
}
class WorkItem {
final String? id;
final ActivityMaster? activityMaster;
final WorkArea? workArea;
final double? plannedWork;
final double? completedWork;
final List<String> preSignedUrls;
WorkItem({
this.id,
this.activityMaster,
this.workArea,
this.plannedWork,
this.completedWork,
this.preSignedUrls = const [],
});
factory WorkItem.fromJson(Map<String, dynamic> json) {
return WorkItem(
id: json['id']?.toString(),
activityMaster: json['activityMaster'] != null
? ActivityMaster.fromJson(json['activityMaster'])
: null,
workArea:
json['workArea'] != null ? WorkArea.fromJson(json['workArea']) : null,
plannedWork: (json['plannedWork'] as num?)?.toDouble(),
completedWork: (json['completedWork'] as num?)?.toDouble(),
preSignedUrls: (json['preSignedUrls'] as List<dynamic>?)
?.map((e) => e.toString())
.toList() ??
[],
);
}
}
class ActivityMaster {
final String? id; // Added
final String activityName;
ActivityMaster({
this.id,
required this.activityName,
});
factory ActivityMaster.fromJson(Map<String, dynamic> json) {
return ActivityMaster(
id: json['id']?.toString(),
activityName: json['activityName'] ?? '',
);
}
}
class WorkArea {
final String? id; // Added
final String areaName;
final Floor? floor;
WorkArea({
this.id,
required this.areaName,
this.floor,
});
factory WorkArea.fromJson(Map<String, dynamic> json) {
return WorkArea(
id: json['id']?.toString(),
areaName: json['areaName'] ?? '',
floor: json['floor'] != null ? Floor.fromJson(json['floor']) : null,
);
}
}
class Floor {
final String floorName;
final Building? building;
Floor({required this.floorName, this.building});
factory Floor.fromJson(Map<String, dynamic> json) {
return Floor(
floorName: json['floorName'] ?? '',
building:
json['building'] != null ? Building.fromJson(json['building']) : null,
);
}
}
class Building {
final String name;
Building({required this.name});
factory Building.fromJson(Map<String, dynamic> json) {
return Building(name: json['name'] ?? '');
}
}
class AssignedBy {
final String id;
final String firstName;
final String? lastName;
AssignedBy({
required this.id,
required this.firstName,
this.lastName,
});
factory AssignedBy.fromJson(Map<String, dynamic> json) {
return AssignedBy(
id: json['id']?.toString() ?? '',
firstName: json['firstName'] ?? '',
lastName: json['lastName'],
);
}
}
class TeamMember {
final String id;
final String firstName;
final String? lastName;
TeamMember({
required this.id,
required this.firstName,
this.lastName,
});
factory TeamMember.fromJson(Map<String, dynamic> json) {
return TeamMember(
id: json['id']?.toString() ?? '',
firstName: json['firstName']?.toString() ?? '',
lastName: json['lastName']?.toString(),
);
}
}
class Comment {
final String comment;
final TeamMember commentedBy;
final DateTime timestamp;
final List<String> preSignedUrls;
Comment({
required this.comment,
required this.commentedBy,
required this.timestamp,
required this.preSignedUrls,
});
factory Comment.fromJson(Map<String, dynamic> json) {
return Comment(
comment: json['comment']?.toString() ?? '',
commentedBy: json['employee'] != null
? TeamMember.fromJson(json['employee'])
: TeamMember(id: '', firstName: '', lastName: null),
timestamp: DateTime.parse(json['commentDate'] ?? ''),
preSignedUrls: (json['preSignedUrls'] as List<dynamic>?)
?.map((e) => e.toString())
.toList() ??
[],
);
}
}

View File

@ -1,144 +0,0 @@
import 'package:flutter/material.dart';
import 'package:marco/controller/permission_controller.dart';
import 'package:marco/controller/task_Planning/daily_task_Planning_controller.dart';
import 'package:marco/helpers/widgets/my_text.dart';
class DailyTaskPlanningFilter extends StatelessWidget {
final DailyTaskPlanningController controller;
final PermissionController permissionController;
const DailyTaskPlanningFilter({
super.key,
required this.controller,
required this.permissionController,
});
@override
Widget build(BuildContext context) {
String? tempSelectedProjectId = '654563563645';
bool showProjectList = false;
final accessibleProjects = controller.projects
.where((project) =>
permissionController.isUserAssignedToProject(project.id.toString()))
.toList();
return StatefulBuilder(builder: (context, setState) {
List<Widget> filterWidgets;
if (showProjectList) {
filterWidgets = accessibleProjects.isEmpty
? [
Padding(
padding: EdgeInsets.all(12.0),
child: Center(
child: MyText.titleSmall(
'No Projects Assigned',
fontWeight: 600,
),
),
),
]
: accessibleProjects.map((project) {
final isSelected =
tempSelectedProjectId == project.id.toString();
return ListTile(
dense: true,
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
title: MyText.titleSmall(project.name),
trailing: isSelected ? const Icon(Icons.check) : null,
onTap: () {
setState(() {
tempSelectedProjectId = project.id.toString();
showProjectList = false;
});
},
);
}).toList();
} else {
final selectedProject = accessibleProjects.isNotEmpty
? accessibleProjects.firstWhere(
(p) => p.id.toString() == tempSelectedProjectId,
orElse: () => accessibleProjects[0],
)
: null;
final selectedProjectName = selectedProject?.name ?? "Select Project";
filterWidgets = [
Padding(
padding: EdgeInsets.fromLTRB(16, 12, 16, 4),
child: Align(
alignment: Alignment.centerLeft,
child: MyText.titleSmall(
'Select Project',
fontWeight: 600,
),
),
),
ListTile(
dense: true,
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
title: MyText.titleSmall(selectedProjectName),
trailing: const Icon(Icons.arrow_drop_down),
onTap: () => setState(() => showProjectList = true),
),
];
}
return Padding(
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).viewInsets.bottom,
),
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.only(top: 12, bottom: 8),
child: Center(
child: Container(
width: 40,
height: 4,
decoration: BoxDecoration(
color: Colors.grey[400],
borderRadius: BorderRadius.circular(4),
),
),
),
),
...filterWidgets,
const Divider(),
Padding(
padding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: SizedBox(
width: double.infinity,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Color.fromARGB(255, 95, 132, 255),
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
child: MyText.titleSmall(
'Apply Filter',
fontWeight: 600,
color: Colors.white,
),
onPressed: () {
Navigator.pop(context, {
'projectId': tempSelectedProjectId,
});
},
),
),
),
],
),
),
);
});
}
}

View File

@ -1,246 +0,0 @@
class TaskPlanningDetailsModel {
final List<Building> buildings;
final String id;
final String name;
final String projectAddress;
final String contactPerson;
final DateTime startDate;
final DateTime endDate;
final String projectStatusId;
TaskPlanningDetailsModel({
required this.buildings,
required this.id,
required this.name,
required this.projectAddress,
required this.contactPerson,
required this.startDate,
required this.endDate,
required this.projectStatusId,
});
factory TaskPlanningDetailsModel.fromJson(Map<String, dynamic> json) {
return TaskPlanningDetailsModel(
buildings: (json['buildings'] as List<dynamic>?)
?.map((b) => Building.fromJson(b))
.toList() ??
[],
id: json['id'],
name: json['name'],
projectAddress: json['projectAddress'],
contactPerson: json['contactPerson'],
startDate: DateTime.parse(json['startDate']),
endDate: DateTime.parse(json['endDate']),
projectStatusId: json['projectStatusId'],
);
}
}
class Building {
final String id;
final String name;
final String description;
final List<Floor> floors;
Building({
required this.id,
required this.name,
required this.description,
required this.floors,
});
factory Building.fromJson(Map<String, dynamic> json) {
return Building(
id: json['id'],
name: json['name'],
description: json['description'],
floors: (json['floors'] as List).map((f) => Floor.fromJson(f)).toList(),
);
}
}
class Floor {
final String id;
final String floorName;
final List<WorkArea> workAreas;
Floor({
required this.id,
required this.floorName,
required this.workAreas,
});
factory Floor.fromJson(Map<String, dynamic> json) {
return Floor(
id: json['id'],
floorName: json['floorName'],
workAreas:
(json['workAreas'] as List).map((w) => WorkArea.fromJson(w)).toList(),
);
}
}
class WorkArea {
final String id;
final String areaName;
final List<WorkItemWrapper> workItems;
WorkArea({
required this.id,
required this.areaName,
required this.workItems,
});
factory WorkArea.fromJson(Map<String, dynamic> json) {
return WorkArea(
id: json['id'],
areaName: json['areaName'],
workItems: (json['workItems'] as List)
.map((w) => WorkItemWrapper.fromJson(w))
.toList(),
);
}
}
class WorkItemWrapper {
final String workItemId;
final WorkItem workItem;
WorkItemWrapper({
required this.workItemId,
required this.workItem,
});
factory WorkItemWrapper.fromJson(Map<String, dynamic> json) {
return WorkItemWrapper(
workItemId: json['workItemId'],
workItem: WorkItem.fromJson(json['workItem']),
);
}
}
class WorkItem {
final String? id;
final String? activityId;
final String? workCategoryId;
final String? workAreaId;
final WorkAreaBasic? workArea;
final ActivityMaster? activityMaster;
final WorkCategoryMaster? workCategoryMaster;
final double? plannedWork;
final double? completedWork;
final String? description;
final double? todaysAssigned;
final DateTime? taskDate;
final String? tenantId;
final Tenant? tenant;
WorkItem({
this.id,
this.activityId,
this.workCategoryId,
this.workAreaId,
this.workArea,
this.activityMaster,
this.workCategoryMaster,
this.description,
this.plannedWork,
this.completedWork,
this.todaysAssigned,
this.taskDate,
this.tenantId,
this.tenant,
});
factory WorkItem.fromJson(Map<String, dynamic> json) {
return WorkItem(
id: json['id'] as String?,
activityId: json['activityId'] as String?,
workCategoryId: json['workCategoryId'] as String?,
workAreaId: json['workAreaId'] as String?,
workArea: json['workArea'] != null
? WorkAreaBasic.fromJson(json['workArea'] as Map<String, dynamic>)
: null,
activityMaster: json['activityMaster'] != null
? ActivityMaster.fromJson(
json['activityMaster'] as Map<String, dynamic>)
: null,
workCategoryMaster: json['workCategoryMaster'] != null
? WorkCategoryMaster.fromJson(
json['workCategoryMaster'] as Map<String, dynamic>)
: null,
plannedWork: json['plannedWork'] != null
? (json['plannedWork'] as num).toDouble()
: null,
completedWork: json['completedWork'] != null
? (json['completedWork'] as num).toDouble()
: null,
todaysAssigned: json['todaysAssigned'] != null
? (json['todaysAssigned'] as num).toDouble()
: null,
description: json['description'] as String?,
taskDate:
json['taskDate'] != null ? DateTime.tryParse(json['taskDate']) : null,
tenantId: json['tenantId'] as String?,
tenant: json['tenant'] != null
? Tenant.fromJson(json['tenant'] as Map<String, dynamic>)
: null,
);
}
}
class WorkAreaBasic {
final String? id;
final String? name;
WorkAreaBasic({this.id, this.name});
factory WorkAreaBasic.fromJson(Map<String, dynamic> json) {
return WorkAreaBasic(
id: json['id'] as String?,
name: json['name'] as String?,
);
}
}
class ActivityMaster {
final String? id;
final String? name;
ActivityMaster({this.id, this.name});
factory ActivityMaster.fromJson(Map<String, dynamic> json) {
return ActivityMaster(
id: json['id'] as String?,
name: json['activityName'] as String?,
);
}
}
class WorkCategoryMaster {
final String? id;
final String? name;
WorkCategoryMaster({this.id, this.name});
factory WorkCategoryMaster.fromJson(Map<String, dynamic> json) {
return WorkCategoryMaster(
id: json['id'] as String?,
name: json['name'] as String?,
);
}
}
class Tenant {
final String? id;
final String? name;
Tenant({this.id, this.name});
factory Tenant.fromJson(Map<String, dynamic> json) {
return Tenant(
id: json['id'] as String?,
name: json['name'] as String?,
);
}
}

View File

@ -1,31 +0,0 @@
class WorkCategoryModel {
final String id;
final String name;
final String description;
final bool isSystem;
WorkCategoryModel({
required this.id,
required this.name,
required this.description,
required this.isSystem,
});
factory WorkCategoryModel.fromJson(Map<String, dynamic> json) {
return WorkCategoryModel(
id: json['id'] ?? '',
name: json['name'] ?? '',
description: json['description'] ?? '',
isSystem: json['isSystem'] ?? false,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'description': description,
'isSystem': isSystem,
};
}
}

View File

@ -1,500 +0,0 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:marco/controller/task_planning/report_task_action_controller.dart';
import 'package:marco/helpers/utils/mixins/ui_mixin.dart';
import 'package:marco/helpers/widgets/my_spacing.dart';
import 'package:marco/helpers/widgets/my_text.dart';
import 'package:marco/helpers/widgets/my_text_style.dart';
import 'package:marco/helpers/widgets/avatar.dart';
import 'package:marco/helpers/widgets/my_team_model_sheet.dart';
import 'package:marco/helpers/widgets/image_viewer_dialog.dart';
import 'package:marco/model/dailyTaskPlanning/create_task_botom_sheet.dart';
import 'package:marco/model/dailyTaskPlanning/report_action_widgets.dart';
import 'package:marco/helpers/utils/base_bottom_sheet.dart';
class ReportActionBottomSheet extends StatefulWidget {
final Map<String, dynamic> taskData;
final VoidCallback? onCommentSuccess;
final String taskDataId;
final String workAreaId;
final String activityId;
final VoidCallback onReportSuccess;
const ReportActionBottomSheet({
super.key,
required this.taskData,
this.onCommentSuccess,
required this.taskDataId,
required this.workAreaId,
required this.activityId,
required this.onReportSuccess,
});
@override
State<ReportActionBottomSheet> createState() =>
_ReportActionBottomSheetState();
}
class _Member {
final String firstName;
_Member(this.firstName);
}
class _ReportActionBottomSheetState extends State<ReportActionBottomSheet>
with UIMixin {
late ReportTaskActionController controller;
@override
void initState() {
super.initState();
controller = Get.put(
ReportTaskActionController(),
tag: widget.taskData['taskId'] ?? '',
);
controller.fetchWorkStatuses();
final data = widget.taskData;
controller.basicValidator.getController('approved_task')?.text =
data['approvedTask']?.toString() ?? '';
controller.basicValidator.getController('assigned_date')?.text =
data['assignedOn'] ?? '';
controller.basicValidator.getController('assigned_by')?.text =
data['assignedBy'] ?? '';
controller.basicValidator.getController('work_area')?.text =
data['location'] ?? '';
controller.basicValidator.getController('activity')?.text =
data['activity'] ?? '';
controller.basicValidator.getController('planned_work')?.text =
data['plannedWork'] ?? '';
controller.basicValidator.getController('completed_work')?.text =
data['completedWork'] ?? '';
controller.basicValidator.getController('team_members')?.text =
(data['teamMembers'] as List<dynamic>).join(', ');
controller.basicValidator.getController('assigned')?.text =
data['assigned'] ?? '';
controller.basicValidator.getController('task_id')?.text =
widget.taskDataId;
controller.basicValidator.getController('comment')?.clear();
controller.selectedImages.clear();
}
@override
Widget build(BuildContext context) {
return GetBuilder<ReportTaskActionController>(
tag: widget.taskData['taskId'] ?? '',
builder: (controller) {
return BaseBottomSheet(
title: "Take Report Action",
isSubmitting: controller.isLoading.value,
onCancel: () => Navigator.of(context).pop(),
onSubmit: () async {}, // not used since buttons moved
showButtons: false, // disable internal buttons
child: _buildForm(context, controller),
);
},
);
}
Widget _buildForm(
BuildContext context, ReportTaskActionController controller) {
return Form(
key: controller.basicValidator.formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 📋 Task Details
buildRow("Assigned By",
controller.basicValidator.getController('assigned_by')?.text,
icon: Icons.person_outline),
buildRow("Work Area",
controller.basicValidator.getController('work_area')?.text,
icon: Icons.place_outlined),
buildRow("Activity",
controller.basicValidator.getController('activity')?.text,
icon: Icons.assignment_outlined),
buildRow("Planned Work",
controller.basicValidator.getController('planned_work')?.text,
icon: Icons.schedule_outlined),
buildRow("Completed Work",
controller.basicValidator.getController('completed_work')?.text,
icon: Icons.done_all_outlined),
buildTeamMembers(),
MySpacing.height(8),
// Approved Task Field
Row(
children: [
Icon(Icons.check_circle_outline,
size: 18, color: Colors.grey[700]),
MySpacing.width(8),
MyText.titleSmall("Approved Task:", fontWeight: 600),
],
),
MySpacing.height(10),
TextFormField(
controller:
controller.basicValidator.getController('approved_task'),
keyboardType: TextInputType.number,
validator: (value) {
if (value == null || value.isEmpty) return 'Required';
if (int.tryParse(value) == null) return 'Must be a number';
return null;
},
decoration: InputDecoration(
hintText: "eg: 5",
hintStyle: MyTextStyle.bodySmall(xMuted: true),
border: outlineInputBorder,
contentPadding: MySpacing.all(16),
floatingLabelBehavior: FloatingLabelBehavior.never,
),
),
MySpacing.height(10),
if ((widget.taskData['reportedPreSignedUrls'] as List<dynamic>?)
?.isNotEmpty ==
true)
buildReportedImagesSection(
imageUrls: List<String>.from(
widget.taskData['reportedPreSignedUrls'] ?? []),
context: context,
),
MySpacing.height(10),
MyText.titleSmall("Report Actions", fontWeight: 600),
MySpacing.height(10),
Obx(() {
if (controller.isLoadingWorkStatus.value)
return const CircularProgressIndicator();
return PopupMenuButton<String>(
onSelected: (String value) {
controller.selectedWorkStatusName.value = value;
controller.showAddTaskCheckbox.value = true;
},
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12)),
itemBuilder: (BuildContext context) {
return controller.workStatus.map((status) {
return PopupMenuItem<String>(
value: status.name,
child: Row(
children: [
Radio<String>(
value: status.name,
groupValue: controller.selectedWorkStatusName.value,
onChanged: (_) => Navigator.pop(context, status.name),
),
const SizedBox(width: 8),
MyText.bodySmall(status.name),
],
),
);
}).toList();
},
child: Container(
padding: MySpacing.xy(16, 12),
decoration: BoxDecoration(
border: Border.all(color: Colors.grey.shade400),
borderRadius: BorderRadius.circular(12),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
MyText.bodySmall(
controller.selectedWorkStatusName.value.isEmpty
? "Select Work Status"
: controller.selectedWorkStatusName.value,
color: Colors.black87,
),
const Icon(Icons.arrow_drop_down, size: 20),
],
),
),
);
}),
MySpacing.height(10),
Obx(() {
if (!controller.showAddTaskCheckbox.value)
return const SizedBox.shrink();
return Theme(
data: Theme.of(context).copyWith(
checkboxTheme: CheckboxThemeData(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(4),
),
side: const BorderSide(
color: Colors.black, width: 2),
fillColor: MaterialStateProperty.resolveWith<Color>((states) {
if (states.contains(MaterialState.selected)) {
return Colors.blueAccent;
}
return Colors.white;
}),
checkColor:
MaterialStateProperty.all(Colors.white),
),
),
child: CheckboxListTile(
title: MyText.titleSmall("Add new task", fontWeight: 600),
value: controller.isAddTaskChecked.value,
onChanged: (val) =>
controller.isAddTaskChecked.value = val ?? false,
controlAffinity: ListTileControlAffinity.leading,
contentPadding: EdgeInsets.zero,
),
);
}),
MySpacing.height(24),
// Comment Field
Row(
children: [
Icon(Icons.comment_outlined, size: 18, color: Colors.grey[700]),
MySpacing.width(8),
MyText.titleSmall("Comment:", fontWeight: 600),
],
),
MySpacing.height(8),
TextFormField(
validator: controller.basicValidator.getValidation('comment'),
controller: controller.basicValidator.getController('comment'),
keyboardType: TextInputType.text,
decoration: InputDecoration(
hintText: "eg: Work done successfully",
hintStyle: MyTextStyle.bodySmall(xMuted: true),
border: outlineInputBorder,
contentPadding: MySpacing.all(16),
floatingLabelBehavior: FloatingLabelBehavior.never,
),
),
MySpacing.height(16),
// 📸 Image Attachments
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(Icons.camera_alt_outlined,
size: 18, color: Colors.grey[700]),
MySpacing.width(8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MyText.titleSmall("Attach Photos:", fontWeight: 600),
MySpacing.height(12),
],
),
),
],
),
Obx(() {
final images = controller.selectedImages;
return buildImagePickerSection(
images: images,
onCameraTap: () => controller.pickImages(fromCamera: true),
onUploadTap: () => controller.pickImages(fromCamera: false),
onRemoveImage: (index) => controller.removeImageAt(index),
onPreviewImage: (index) {
showDialog(
context: context,
builder: (_) => ImageViewerDialog(
imageSources: images,
initialIndex: index,
),
);
},
);
}),
MySpacing.height(12),
// Submit/Cancel Buttons moved here
Row(
children: [
Expanded(
child: ElevatedButton.icon(
onPressed: () => Navigator.of(context).pop(),
icon: const Icon(Icons.close, color: Colors.white),
label: MyText.bodyMedium("Cancel",
color: Colors.white, fontWeight: 600),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.grey,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12)),
padding: const EdgeInsets.symmetric(vertical: 8),
),
),
),
const SizedBox(width: 12),
Expanded(
child: ElevatedButton.icon(
onPressed: controller.isLoading.value
? null
: () async {
if (controller.basicValidator.validateForm()) {
final selectedStatusName =
controller.selectedWorkStatusName.value;
final selectedStatus = controller.workStatus
.firstWhereOrNull(
(s) => s.name == selectedStatusName);
final reportActionId =
selectedStatus?.id.toString() ?? '';
final approvedTaskCount = controller.basicValidator
.getController('approved_task')
?.text
.trim() ??
'';
final shouldShowAddTaskSheet =
controller.isAddTaskChecked.value;
final success = await controller.approveTask(
projectId: controller.basicValidator
.getController('task_id')
?.text ??
'',
comment: controller.basicValidator
.getController('comment')
?.text ??
'',
images: controller.selectedImages,
reportActionId: reportActionId,
approvedTaskCount: approvedTaskCount,
);
if (success) {
Navigator.of(context).pop();
if (shouldShowAddTaskSheet) {
await Future.delayed(
const Duration(milliseconds: 100));
showCreateTaskBottomSheet(
workArea: widget.taskData['location'] ?? '',
activity: widget.taskData['activity'] ?? '',
completedWork:
widget.taskData['completedWork'] ?? '',
unit: widget.taskData['unit'] ?? '',
parentTaskId: widget.taskDataId,
plannedTask: int.tryParse(
widget.taskData['plannedWork'] ??
'0') ??
0,
activityId: widget.activityId,
workAreaId: widget.workAreaId,
onSubmit: () => Navigator.of(context).pop(),
onCategoryChanged: (category) {},
);
}
widget.onReportSuccess.call();
}
}
},
icon: const Icon(Icons.check_circle_outline,
color: Colors.white),
label: MyText.bodyMedium(
controller.isLoading.value ? "Submitting..." : "Submit",
color: Colors.white,
fontWeight: 600,
),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.indigo,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12)),
padding: const EdgeInsets.symmetric(vertical: 8),
),
),
),
],
),
MySpacing.height(12),
// 💬 Previous Comments List (only below submit)
if ((widget.taskData['taskComments'] as List<dynamic>?)?.isNotEmpty ==
true) ...[
Row(
children: [
MySpacing.width(10),
Icon(Icons.chat_bubble_outline,
size: 18, color: Colors.grey[700]),
MySpacing.width(8),
MyText.titleSmall("Comments", fontWeight: 600),
],
),
MySpacing.height(12),
buildCommentList(
List<Map<String, dynamic>>.from(
widget.taskData['taskComments'] as List),
context,
timeAgo,
),
],
],
),
);
}
Widget buildTeamMembers() {
final teamMembersText =
controller.basicValidator.getController('team_members')?.text ?? '';
final members = teamMembersText
.split(',')
.map((e) => e.trim())
.where((e) => e.isNotEmpty)
.toList();
return Padding(
padding: const EdgeInsets.only(bottom: 16),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
MyText.titleSmall("Team Members:", fontWeight: 600),
MySpacing.width(12),
GestureDetector(
onTap: () {
TeamBottomSheet.show(
context: context,
teamMembers: members.map((name) => _Member(name)).toList(),
);
},
child: SizedBox(
height: 32,
width: 100,
child: Stack(
children: [
for (int i = 0; i < members.length.clamp(0, 3); i++)
Positioned(
left: i * 24.0,
child: Tooltip(
message: members[i],
child: Avatar(
firstName: members[i],
lastName: '',
size: 32,
),
),
),
if (members.length > 3)
Positioned(
left: 2 * 24.0,
child: CircleAvatar(
radius: 16,
backgroundColor: Colors.grey.shade300,
child: MyText.bodyMedium(
'+${members.length - 3}',
style: const TextStyle(
fontSize: 12, color: Colors.black87),
),
),
),
],
),
),
),
],
),
);
}
}

View File

@ -1,392 +0,0 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:marco/helpers/widgets/my_button.dart';
import 'package:marco/helpers/widgets/my_spacing.dart';
import 'package:marco/helpers/widgets/my_text.dart';
import 'package:marco/helpers/widgets/image_viewer_dialog.dart';
import 'package:marco/helpers/widgets/avatar.dart';
import 'package:get/get.dart';
/// Show labeled row with optional icon
Widget buildRow(String label, String? value, {IconData? icon}) {
return Padding(
padding: const EdgeInsets.only(bottom: 16),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (icon != null)
Padding(
padding: const EdgeInsets.only(right: 8.0, top: 2),
child: Icon(icon, size: 18, color: Colors.grey[700]),
),
MyText.titleSmall("$label:", fontWeight: 600),
MySpacing.width(12),
Expanded(
child: MyText.bodyMedium(value?.isNotEmpty == true ? value! : "-"),
),
],
),
);
}
/// Show uploaded network images
Widget buildReportedImagesSection({
required List<String> imageUrls,
required BuildContext context,
String title = "Reported Images",
}) {
if (imageUrls.isEmpty) return const SizedBox();
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MySpacing.height(8),
Row(
children: [
Icon(Icons.image_outlined, size: 18, color: Colors.grey[700]),
MySpacing.width(8),
MyText.titleSmall(title, fontWeight: 600),
],
),
MySpacing.height(8),
SizedBox(
height: 70,
child: ListView.separated(
scrollDirection: Axis.horizontal,
itemCount: imageUrls.length,
separatorBuilder: (_, __) => const SizedBox(width: 12),
itemBuilder: (context, index) {
final url = imageUrls[index];
return GestureDetector(
onTap: () {
showDialog(
context: context,
builder: (_) => ImageViewerDialog(
imageSources: imageUrls,
initialIndex: index,
),
);
},
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Image.network(
url,
width: 70,
height: 70,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) => Container(
width: 70,
height: 70,
color: Colors.grey.shade200,
child: Icon(Icons.broken_image, color: Colors.grey[600]),
),
),
),
);
},
),
),
MySpacing.height(16),
],
);
}
/// Local image picker preview (with file images)
Widget buildImagePickerSection({
required List<File> images,
required VoidCallback onCameraTap,
required VoidCallback onUploadTap,
required void Function(int index) onRemoveImage,
required void Function(int initialIndex) onPreviewImage,
}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (images.isEmpty)
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: 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: (_, __) => const SizedBox(width: 12),
itemBuilder: (context, index) {
final file = images[index];
return Stack(
children: [
GestureDetector(
onTap: () => onPreviewImage(index),
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Image.file(
file,
height: 70,
width: 70,
fit: BoxFit.cover,
),
),
),
Positioned(
top: 4,
right: 4,
child: GestureDetector(
onTap: () => onRemoveImage(index),
child: Container(
decoration: const 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: onCameraTap,
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: onUploadTap,
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),
],
),
),
),
],
),
],
);
}
/// Comment list widget
Widget buildCommentList(
List<Map<String, dynamic>> comments, BuildContext context, String Function(String) timeAgo) {
comments.sort((a, b) {
final aDate = DateTime.tryParse(a['date'] ?? '') ??
DateTime.fromMillisecondsSinceEpoch(0);
final bDate = DateTime.tryParse(b['date'] ?? '') ??
DateTime.fromMillisecondsSinceEpoch(0);
return bDate.compareTo(aDate); // newest first
});
return SizedBox(
height: 300,
child: ListView.builder(
padding: const EdgeInsets.symmetric(vertical: 8),
itemCount: comments.length,
itemBuilder: (context, index) {
final comment = comments[index];
final commentText = comment['text'] ?? '-';
final commentedBy = comment['commentedBy'] ?? 'Unknown';
final relativeTime = timeAgo(comment['date'] ?? '');
final imageUrls = List<String>.from(comment['preSignedUrls'] ?? []);
return Container(
margin: const EdgeInsets.symmetric(vertical: 8),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.grey.shade200,
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Avatar(
firstName: commentedBy.split(' ').first,
lastName: commentedBy.split(' ').length > 1
? commentedBy.split(' ').last
: '',
size: 32,
),
const SizedBox(width: 12),
Expanded(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
MyText.bodyMedium(commentedBy,
fontWeight: 700, color: Colors.black87),
MyText.bodySmall(
relativeTime,
fontSize: 12,
color: Colors.black54,
),
],
),
),
],
),
const SizedBox(height: 12),
MyText.bodyMedium(commentText,
fontWeight: 500, color: Colors.black87),
const SizedBox(height: 12),
if (imageUrls.isNotEmpty) ...[
Row(
children: [
Icon(Icons.attach_file_outlined,
size: 18, color: Colors.grey[700]),
MySpacing.width(8),
MyText.bodyMedium('Attachments',
fontWeight: 600, color: Colors.black87),
],
),
const SizedBox(height: 8),
SizedBox(
height: 60,
child: ListView.separated(
scrollDirection: Axis.horizontal,
itemCount: imageUrls.length,
itemBuilder: (context, imageIndex) {
final imageUrl = imageUrls[imageIndex];
return GestureDetector(
onTap: () {
showDialog(
context: context,
builder: (_) => ImageViewerDialog(
imageSources: imageUrls,
initialIndex: imageIndex,
),
);
},
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Image.network(
imageUrl,
width: 60,
height: 60,
fit: BoxFit.cover,
),
),
);
},
separatorBuilder: (_, __) => const SizedBox(width: 12),
),
),
]
],
),
);
},
),
);
}
/// Cancel + Submit buttons
Widget buildCommentActionButtons({
required VoidCallback onCancel,
required Future<void> Function() onSubmit,
required RxBool isLoading,
}) {
return Row(
children: [
Expanded(
child: OutlinedButton.icon(
onPressed: onCancel,
icon: const Icon(Icons.close, color: Colors.red, size: 18),
label:
MyText.bodyMedium("Cancel", color: Colors.red, fontWeight: 600),
style: OutlinedButton.styleFrom(
side: const BorderSide(color: Colors.red),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 14),
),
),
),
const SizedBox(width: 16),
Expanded(
child: Obx(() {
return ElevatedButton.icon(
onPressed: isLoading.value ? null : () => onSubmit(),
icon: isLoading.value
? const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
),
)
: const Icon(Icons.send, color: Colors.white, size: 18),
label: isLoading.value
? const SizedBox()
: MyText.bodyMedium("Submit",
color: Colors.white, fontWeight: 600),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.indigo,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 14),
),
);
}),
),
],
);
}
/// Converts a UTC timestamp to a relative time string
String timeAgo(String dateString) {
try {
DateTime date = DateTime.parse(dateString + "Z").toLocal();
final now = DateTime.now();
final difference = now.difference(date);
if (difference.inDays > 8) {
return "${date.day.toString().padLeft(2, '0')}-${date.month.toString().padLeft(2, '0')}-${date.year}";
} else if (difference.inDays >= 1) {
return '${difference.inDays} day${difference.inDays > 1 ? 's' : ''} ago';
} else if (difference.inHours >= 1) {
return '${difference.inHours} hr${difference.inHours > 1 ? 's' : ''} ago';
} else if (difference.inMinutes >= 1) {
return '${difference.inMinutes} min${difference.inMinutes > 1 ? 's' : ''} ago';
} else {
return 'just now';
}
} catch (e) {
return '';
}
}

View File

@ -1,310 +0,0 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:marco/controller/task_planning/report_task_controller.dart';
import 'package:marco/helpers/utils/mixins/ui_mixin.dart';
import 'package:marco/helpers/widgets/my_button.dart';
import 'package:marco/helpers/widgets/my_spacing.dart';
import 'package:marco/helpers/widgets/my_text.dart';
import 'package:marco/helpers/widgets/my_text_style.dart';
import 'package:marco/helpers/utils/base_bottom_sheet.dart';
class ReportTaskBottomSheet extends StatefulWidget {
final Map<String, dynamic> taskData;
final VoidCallback? onReportSuccess;
const ReportTaskBottomSheet({
super.key,
required this.taskData,
this.onReportSuccess,
});
@override
State<ReportTaskBottomSheet> createState() => _ReportTaskBottomSheetState();
}
class _ReportTaskBottomSheetState extends State<ReportTaskBottomSheet>
with UIMixin {
late final ReportTaskController controller;
@override
void initState() {
super.initState();
controller = Get.put(
ReportTaskController(),
tag: widget.taskData['taskId'] ?? UniqueKey().toString(),
);
_preFillFormFields();
}
void _preFillFormFields() {
final data = widget.taskData;
final v = controller.basicValidator;
v.getController('assigned_date')?.text = data['assignedOn'] ?? '';
v.getController('assigned_by')?.text = data['assignedBy'] ?? '';
v.getController('work_area')?.text = data['location'] ?? '';
v.getController('activity')?.text = data['activity'] ?? '';
v.getController('team_size')?.text = data['teamSize']?.toString() ?? '';
v.getController('assigned')?.text = data['assigned'] ?? '';
v.getController('task_id')?.text = data['taskId'] ?? '';
v.getController('completed_work')?.clear();
v.getController('comment')?.clear();
}
@override
Widget build(BuildContext context) {
return Obx(() {
return BaseBottomSheet(
title: "Report Task",
isSubmitting: controller.reportStatus.value == ApiStatus.loading,
onCancel: () => Navigator.of(context).pop(),
onSubmit: _handleSubmit,
child: Form(
key: controller.basicValidator.formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildRow("Assigned Date", controller.basicValidator.getController('assigned_date')?.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(
"Assigned",
"${controller.basicValidator.getController('assigned')?.text ?? '-'} "
"of ${widget.taskData['pendingWork'] ?? '-'} Pending",
),
_buildCompletedWorkField(),
_buildCommentField(),
Obx(() => _buildImageSection()),
],
),
),
);
});
}
Future<void> _handleSubmit() async {
final v = controller.basicValidator;
if (v.validateForm()) {
final success = await controller.reportTask(
projectId: v.getController('task_id')?.text ?? '',
comment: v.getController('comment')?.text ?? '',
completedTask: int.tryParse(v.getController('completed_work')?.text ?? '') ?? 0,
checklist: [],
reportedDate: DateTime.now(),
images: controller.selectedImages,
);
if (success) {
widget.onReportSuccess?.call();
}
}
}
Widget _buildRow(String label, String? value) {
final icons = {
"Assigned Date": Icons.calendar_today_outlined,
"Assigned By": Icons.person_outline,
"Work Area": Icons.place_outlined,
"Activity": Icons.run_circle_outlined,
"Team Size": Icons.group_outlined,
"Assigned": Icons.assignment_turned_in_outlined,
};
return Padding(
padding: const EdgeInsets.only(bottom: 16),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(icons[label] ?? Icons.info_outline, size: 18, color: Colors.grey[700]),
MySpacing.width(8),
MyText.titleSmall("$label:", fontWeight: 600),
MySpacing.width(12),
Expanded(
child: MyText.bodyMedium(value?.trim().isNotEmpty == true ? value!.trim() : "-"),
),
],
),
);
}
Widget _buildCompletedWorkField() {
final pending = widget.taskData['pendingWork'] ?? 0;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.work_outline, size: 18, color: Colors.grey[700]),
MySpacing.width(8),
MyText.titleSmall("Completed Work:", fontWeight: 600),
],
),
MySpacing.height(8),
TextFormField(
controller: controller.basicValidator.getController('completed_work'),
keyboardType: TextInputType.number,
validator: (value) {
if (value == null || value.trim().isEmpty) return 'Please enter completed work';
final completed = int.tryParse(value.trim());
if (completed == null) return 'Enter a valid number';
if (completed > pending) return 'Completed work cannot exceed pending work $pending';
return null;
},
decoration: InputDecoration(
hintText: "eg: 10",
hintStyle: MyTextStyle.bodySmall(xMuted: true),
border: outlineInputBorder,
enabledBorder: outlineInputBorder,
focusedBorder: focusedInputBorder,
contentPadding: MySpacing.all(16),
isCollapsed: true,
floatingLabelBehavior: FloatingLabelBehavior.never,
),
),
MySpacing.height(24),
],
);
}
Widget _buildCommentField() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.comment_outlined, size: 18, color: Colors.grey[700]),
MySpacing.width(8),
MyText.titleSmall("Comment:", fontWeight: 600),
],
),
MySpacing.height(8),
TextFormField(
controller: controller.basicValidator.getController('comment'),
validator: controller.basicValidator.getValidation('comment'),
keyboardType: TextInputType.text,
decoration: InputDecoration(
hintText: "eg: Work done successfully",
hintStyle: MyTextStyle.bodySmall(xMuted: true),
border: outlineInputBorder,
enabledBorder: outlineInputBorder,
focusedBorder: focusedInputBorder,
contentPadding: MySpacing.all(16),
isCollapsed: true,
floatingLabelBehavior: FloatingLabelBehavior.never,
),
),
MySpacing.height(24),
],
);
}
Widget _buildImageSection() {
final images = controller.selectedImages;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.camera_alt_outlined, size: 18, color: Colors.grey[700]),
MySpacing.width(8),
MyText.titleSmall("Attach Photos:", fontWeight: 600),
],
),
MySpacing.height(12),
if (images.isEmpty)
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: 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,
right: 4,
child: GestureDetector(
onTap: () => controller.removeImageAt(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),
],
),
),
),
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),
],
),
),
),
],
),
],
);
}
}

View File

@ -1,210 +0,0 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:marco/model/dailyTaskPlanning/comment_task_bottom_sheet.dart';
import 'package:marco/model/dailyTaskPlanning/report_task_bottom_sheet.dart';
import 'package:marco/model/dailyTaskPlanning/report_action_bottom_sheet.dart';
class TaskActionButtons {
static Widget reportButton({
required BuildContext context,
required dynamic task,
required int completed,
required VoidCallback refreshCallback,
}) {
return OutlinedButton.icon(
icon: const Icon(Icons.report, size: 18, color: Colors.blueAccent),
label: const Text('Report', style: TextStyle(color: Colors.blueAccent)),
style: OutlinedButton.styleFrom(
side: const BorderSide(color: Colors.blueAccent),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
textStyle: const TextStyle(fontSize: 14),
),
onPressed: () {
final activityName =
task.workItem?.activityMaster?.activityName ?? 'N/A';
final assigned = '${(task.plannedTask - completed)}';
final assignedBy =
"${task.assignedBy.firstName} ${task.assignedBy.lastName ?? ''}";
final assignedOn = DateFormat('dd-MM-yyyy').format(task.assignmentDate);
final taskId = task.id;
final location = [
task.workItem?.workArea?.floor?.building?.name,
task.workItem?.workArea?.floor?.floorName,
task.workItem?.workArea?.areaName,
].where((e) => e != null && e.isNotEmpty).join(' > ');
final teamMembers = task.teamMembers.map((e) => e.firstName).toList();
final pendingWork = (task.workItem?.plannedWork ?? 0) -
(task.workItem?.completedWork ?? 0);
final taskData = {
'activity': activityName,
'assigned': assigned,
'taskId': taskId,
'assignedBy': assignedBy,
'completed': completed,
'assignedOn': assignedOn,
'location': location,
'teamSize': task.teamMembers.length,
'teamMembers': teamMembers,
'pendingWork': pendingWork,
};
showModalBottomSheet(
context: context,
isScrollControlled: true,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
),
builder: (ctx) => Padding(
padding: MediaQuery.of(ctx).viewInsets,
child: ReportTaskBottomSheet(
taskData: taskData,
onReportSuccess: refreshCallback,
),
),
);
},
);
}
static Widget commentButton({
required BuildContext context,
required dynamic task,
required VoidCallback refreshCallback,
required String parentTaskID,
required String activityId,
required String workAreaId,
}) {
return OutlinedButton.icon(
icon: const Icon(Icons.comment, size: 18, color: Colors.blueAccent),
label: const Text('Comment', style: TextStyle(color: Colors.blueAccent)),
style: OutlinedButton.styleFrom(
side: const BorderSide(color: Colors.blueAccent),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
textStyle: const TextStyle(fontSize: 14),
),
onPressed: () {
final taskData =
_prepareTaskData(task: task, completed: task.completedTask.toInt());
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (_) => CommentTaskBottomSheet(
taskData: taskData,
taskDataId: parentTaskID,
workAreaId: workAreaId,
activityId: activityId,
onCommentSuccess: () {
refreshCallback();
Navigator.of(context).pop();
},
),
);
},
);
}
static Widget reportActionButton({
required BuildContext context,
required dynamic task,
required int completed,
required VoidCallback refreshCallback,
required String parentTaskID,
required String activityId,
required String workAreaId,
}) {
return OutlinedButton.icon(
icon: const Icon(Icons.report, size: 18, color: Colors.amber),
label: const Text('Take Report Action',
style: TextStyle(color: Colors.amber)),
style: OutlinedButton.styleFrom(
side: const BorderSide(color: Colors.amber),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
textStyle: const TextStyle(fontSize: 14),
),
onPressed: () {
final taskData = _prepareTaskData(task: task, completed: completed);
showModalBottomSheet(
context: context,
isScrollControlled: true,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
),
builder: (ctx) => Padding(
padding: MediaQuery.of(ctx).viewInsets,
child: ReportActionBottomSheet(
taskData: taskData,
taskDataId: parentTaskID,
workAreaId: workAreaId,
activityId: activityId,
onReportSuccess: refreshCallback,
),
),
);
},
);
}
static Map<String, dynamic> _prepareTaskData({
required dynamic task,
required int completed,
}) {
final activityName = task.workItem?.activityMaster?.activityName ?? 'N/A';
final assigned = '${(task.plannedTask - completed)}';
final assignedBy =
"${task.assignedBy.firstName} ${task.assignedBy.lastName ?? ''}";
final assignedOn = DateFormat('yyyy-MM-dd').format(task.assignmentDate);
final taskId = task.id;
final location = [
task.workItem?.workArea?.floor?.building?.name,
task.workItem?.workArea?.floor?.floorName,
task.workItem?.workArea?.areaName,
].where((e) => e != null && e.isNotEmpty).join(' > ');
final teamMembers = task.teamMembers
.map((e) => '${e.firstName} ${e.lastName ?? ''}')
.toList();
final pendingWork =
(task.workItem?.plannedWork ?? 0) - (task.workItem?.completedWork ?? 0);
final taskComments = task.comments.map((comment) {
final isoDate = comment.timestamp.toIso8601String();
final commenterName = comment.commentedBy.firstName.isNotEmpty
? "${comment.commentedBy.firstName} ${comment.commentedBy.lastName ?? ''}"
.trim()
: "Unknown";
return {
'text': comment.comment,
'date': isoDate,
'commentedBy': commenterName,
'preSignedUrls': comment.preSignedUrls,
};
}).toList();
final taskLevelPreSignedUrls = task.reportedPreSignedUrls;
return {
'activity': activityName,
'assigned': assigned,
'taskId': taskId,
'assignedBy': assignedBy,
'completed': completed,
'plannedWork': task.plannedTask.toString(),
'completedWork': completed.toString(),
'assignedOn': assignedOn,
'location': location,
'teamSize': task.teamMembers.length,
'teamMembers': teamMembers,
'pendingWork': pendingWork,
'taskComments': taskComments,
'reportedPreSignedUrls': taskLevelPreSignedUrls,
};
}
}

View File

@ -1,45 +0,0 @@
import 'dart:convert';
import 'package:marco/helpers/services/json_decoder.dart';
import 'package:marco/model/identifier_model.dart';
import 'package:flutter/services.dart';
class TaskListModel extends IdentifierModel {
final String title, description, priority, status;
final DateTime dueDate;
late bool isSelectTask;
TaskListModel(super.id, this.title, this.description, this.priority, this.status, this.dueDate, this.isSelectTask);
static TaskListModel fromJSON(Map<String, dynamic> json) {
JSONDecoder decoder = JSONDecoder(json);
String title = decoder.getString('title');
String description = decoder.getString('description');
String priority = decoder.getString('priority');
String status = decoder.getString('status');
DateTime dueDate = decoder.getDateTime('due_date');
bool isSelectTask = decoder.getBool('isSelectTask');
return TaskListModel(decoder.getId, title, description, priority, status, dueDate, isSelectTask);
}
static List<TaskListModel> listFromJSON(List<dynamic> list) {
return list.map((e) => TaskListModel.fromJSON(e)).toList();
}
static List<TaskListModel>? _dummyList;
static Future<List<TaskListModel>> get dummyList async {
if (_dummyList == null) {
dynamic data = json.decode(await getData());
_dummyList = listFromJSON(data);
}
return _dummyList!;
}
static Future<String> getData() async {
return await rootBundle.loadString('assets/data/task_list.json');
}
}

View File

@ -1,53 +0,0 @@
class WorkStatusResponseModel {
final bool success;
final String message;
final List<WorkStatus> data;
final dynamic errors;
final int statusCode;
final DateTime timestamp;
WorkStatusResponseModel({
required this.success,
required this.message,
required this.data,
required this.errors,
required this.statusCode,
required this.timestamp,
});
factory WorkStatusResponseModel.fromJson(Map<String, dynamic> json) {
return WorkStatusResponseModel(
success: json['success'],
message: json['message'],
data: List<WorkStatus>.from(
json['data'].map((item) => WorkStatus.fromJson(item)),
),
errors: json['errors'],
statusCode: json['statusCode'],
timestamp: DateTime.parse(json['timestamp']),
);
}
}
class WorkStatus {
final String id;
final String name;
final String description;
final bool isSystem;
WorkStatus({
required this.id,
required this.name,
required this.description,
required this.isSystem,
});
factory WorkStatus.fromJson(Map<String, dynamic> json) {
return WorkStatus(
id: json['id'],
name: json['name'],
description: json['description'],
isSystem: json['isSystem'],
);
}
}

View File

@ -1,109 +0,0 @@
class Tenant {
final String id;
final String name;
final String email;
final String? domainName;
final String contactName;
final String contactNumber;
final String? logoImage;
final String? organizationSize;
final Industry? industry;
final TenantStatus? tenantStatus;
Tenant({
required this.id,
required this.name,
required this.email,
this.domainName,
required this.contactName,
required this.contactNumber,
this.logoImage,
this.organizationSize,
this.industry,
this.tenantStatus,
});
factory Tenant.fromJson(Map<String, dynamic> json) {
return Tenant(
id: json['id'] ?? '',
name: json['name'] ?? '',
email: json['email'] ?? '',
domainName: json['domainName'] as String?,
contactName: json['contactName'] ?? '',
contactNumber: json['contactNumber'] ?? '',
logoImage: json['logoImage'] is String ? json['logoImage'] : null,
organizationSize: json['organizationSize'] is String
? json['organizationSize']
: null,
industry: json['industry'] != null
? Industry.fromJson(json['industry'])
: null,
tenantStatus: json['tenantStatus'] != null
? TenantStatus.fromJson(json['tenantStatus'])
: null,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'email': email,
'domainName': domainName,
'contactName': contactName,
'contactNumber': contactNumber,
'logoImage': logoImage,
'organizationSize': organizationSize,
'industry': industry?.toJson(),
'tenantStatus': tenantStatus?.toJson(),
};
}
}
class Industry {
final String id;
final String name;
Industry({
required this.id,
required this.name,
});
factory Industry.fromJson(Map<String, dynamic> json) {
return Industry(
id: json['id'] ?? '',
name: json['name'] ?? '',
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
};
}
}
class TenantStatus {
final String id;
final String name;
TenantStatus({
required this.id,
required this.name,
});
factory TenantStatus.fromJson(Map<String, dynamic> json) {
return TenantStatus(
id: json['id'] ?? '',
name: json['name'] ?? '',
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
};
}
}

View File

@ -1,78 +0,0 @@
class ServiceListResponse {
final bool success;
final String message;
final List<Service> data;
final dynamic errors;
final int statusCode;
final String timestamp;
ServiceListResponse({
required this.success,
required this.message,
required this.data,
this.errors,
required this.statusCode,
required this.timestamp,
});
factory ServiceListResponse.fromJson(Map<String, dynamic> json) {
return ServiceListResponse(
success: json['success'] ?? false,
message: json['message'] ?? '',
data: (json['data'] as List<dynamic>?)
?.map((e) => Service.fromJson(e))
.toList() ??
[],
errors: json['errors'],
statusCode: json['statusCode'] ?? 0,
timestamp: json['timestamp'] ?? '',
);
}
Map<String, dynamic> toJson() {
return {
'success': success,
'message': message,
'data': data.map((e) => e.toJson()).toList(),
'errors': errors,
'statusCode': statusCode,
'timestamp': timestamp,
};
}
}
class Service {
final String id;
final String name;
final String description;
final bool isSystem;
final bool isActive;
Service({
required this.id,
required this.name,
required this.description,
required this.isSystem,
required this.isActive,
});
factory Service.fromJson(Map<String, dynamic> json) {
return Service(
id: json['id'] ?? '',
name: json['name'] ?? '',
description: json['description'] ?? '',
isSystem: json['isSystem'] ?? false,
isActive: json['isActive'] ?? false,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'name': name,
'description': description,
'isSystem': isSystem,
'isActive': isActive,
};
}
}

View File

@ -11,8 +11,6 @@ import 'package:marco/view/error_pages/error_404_screen.dart';
import 'package:marco/view/error_pages/error_500_screen.dart';
import 'package:marco/view/dashboard/dashboard_screen.dart';
import 'package:marco/view/Attendence/attendance_screen.dart';
import 'package:marco/view/taskPlanning/daily_task_planning.dart';
import 'package:marco/view/taskPlanning/daily_progress.dart';
import 'package:marco/view/employees/employees_screen.dart';
import 'package:marco/view/auth/login_option_screen.dart';
import 'package:marco/view/auth/mpin_screen.dart';
@ -55,15 +53,7 @@ getPageRoute() {
name: '/dashboard/employees',
page: () => EmployeesScreen(),
middlewares: [AuthMiddleware()]),
// Daily Task Planning
GetPage(
name: '/dashboard/daily-task-Planning',
page: () => DailyTaskPlanningScreen(),
middlewares: [AuthMiddleware()]),
GetPage(
name: '/dashboard/daily-task-progress',
page: () => DailyProgressReportScreen(),
middlewares: [AuthMiddleware()]),
GetPage(
name: '/dashboard/directory-main-page',
page: () => DirectoryMainScreen(),

View File

@ -1,572 +0,0 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:intl/intl.dart';
import 'package:marco/helpers/theme/app_theme.dart';
import 'package:marco/helpers/utils/mixins/ui_mixin.dart';
import 'package:marco/helpers/utils/my_shadow.dart';
import 'package:marco/helpers/widgets/my_card.dart';
import 'package:marco/helpers/widgets/my_container.dart';
import 'package:marco/helpers/widgets/my_spacing.dart';
import 'package:marco/helpers/widgets/my_text.dart';
import 'package:marco/controller/permission_controller.dart';
import 'package:marco/controller/task_planning/daily_task_controller.dart';
import 'package:marco/model/dailyTaskPlanning/daily_progress_report_filter.dart';
import 'package:marco/helpers/widgets/avatar.dart';
import 'package:marco/controller/project_controller.dart';
import 'package:marco/model/dailyTaskPlanning/task_action_buttons.dart';
import 'package:marco/helpers/widgets/my_custom_skeleton.dart';
import 'package:marco/helpers/utils/permission_constants.dart';
import 'package:marco/helpers/widgets/my_refresh_indicator.dart';
import 'package:marco/controller/tenant/service_controller.dart';
import 'package:marco/helpers/widgets/tenant/service_selector.dart';
class DailyProgressReportScreen extends StatefulWidget {
const DailyProgressReportScreen({super.key});
@override
State<DailyProgressReportScreen> createState() =>
_DailyProgressReportScreenState();
}
class TaskChartData {
final String label;
final num value;
final Color color;
TaskChartData(this.label, this.value, this.color);
}
class _DailyProgressReportScreenState extends State<DailyProgressReportScreen>
with UIMixin {
final DailyTaskController dailyTaskController =
Get.put(DailyTaskController());
final PermissionController permissionController =
Get.find<PermissionController>();
final ProjectController projectController = Get.find<ProjectController>();
final ServiceController serviceController = Get.put(ServiceController());
final ScrollController _scrollController = ScrollController();
@override
void initState() {
super.initState();
_scrollController.addListener(() {
if (_scrollController.position.pixels >=
_scrollController.position.maxScrollExtent - 100 &&
dailyTaskController.hasMore &&
!dailyTaskController.isLoadingMore.value) {
final projectId = dailyTaskController.selectedProjectId;
if (projectId != null && projectId.isNotEmpty) {
dailyTaskController.fetchTaskData(
projectId,
pageNumber: dailyTaskController.currentPage + 1,
pageSize: dailyTaskController.pageSize,
isLoadMore: true,
);
}
}
});
final initialProjectId = projectController.selectedProjectId.value;
if (initialProjectId.isNotEmpty) {
dailyTaskController.selectedProjectId = initialProjectId;
dailyTaskController.fetchTaskData(initialProjectId);
serviceController.fetchServices(initialProjectId);
}
// Update when project changes
ever<String>(projectController.selectedProjectId, (newProjectId) async {
if (newProjectId.isNotEmpty &&
newProjectId != dailyTaskController.selectedProjectId) {
dailyTaskController.selectedProjectId = newProjectId;
await dailyTaskController.fetchTaskData(newProjectId);
await serviceController.fetchServices(newProjectId);
dailyTaskController.update(['daily_progress_report_controller']);
}
});
}
@override
void dispose() {
_scrollController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: PreferredSize(
preferredSize: const Size.fromHeight(72),
child: AppBar(
backgroundColor: const Color(0xFFF5F5F5),
elevation: 0.5,
automaticallyImplyLeading: false,
titleSpacing: 0,
title: Padding(
padding: MySpacing.xy(16, 0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
IconButton(
icon: const Icon(Icons.arrow_back_ios_new,
color: Colors.black, size: 20),
onPressed: () => Get.offNamed('/dashboard'),
),
MySpacing.width(8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
MyText.titleLarge(
'Daily Progress Report',
fontWeight: 700,
color: Colors.black,
),
MySpacing.height(2),
GetBuilder<ProjectController>(
builder: (projectController) {
final projectName =
projectController.selectedProject?.name ??
'Select Project';
return Row(
children: [
const Icon(Icons.work_outline,
size: 14, color: Colors.grey),
MySpacing.width(4),
Expanded(
child: MyText.bodySmall(
projectName,
fontWeight: 600,
overflow: TextOverflow.ellipsis,
color: Colors.grey[700],
),
),
],
);
},
),
],
),
),
],
),
),
),
),
body: SafeArea(
child: MyRefreshIndicator(
onRefresh: _refreshData,
child: CustomScrollView(
physics: const AlwaysScrollableScrollPhysics(),
slivers: [
SliverToBoxAdapter(
child: GetBuilder<DailyTaskController>(
init: dailyTaskController,
tag: 'daily_progress_report_controller',
builder: (controller) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MySpacing.height(flexSpacing),
// --- ADD SERVICE SELECTOR HERE ---
Padding(
padding: MySpacing.x(10),
child: ServiceSelector(
controller: serviceController,
height: 40,
onSelectionChanged: (service) async {
final projectId =
dailyTaskController.selectedProjectId;
if (projectId?.isNotEmpty ?? false) {
await dailyTaskController.fetchTaskData(
projectId!,
serviceIds:
service != null ? [service.id] : null,
pageNumber: 1,
pageSize: 20,
);
}
},
),
),
_buildActionBar(),
Padding(
padding: MySpacing.x(8),
child: _buildDailyProgressReportTab(),
),
],
);
},
),
),
],
),
),
),
);
}
Widget _buildActionBar() {
return Padding(
padding: MySpacing.x(flexSpacing),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
_buildActionItem(
label: "Filter",
icon: Icons.tune,
tooltip: 'Filter Project',
onTap: _openFilterSheet,
),
],
),
);
}
Widget _buildActionItem({
required String label,
required IconData icon,
required String tooltip,
required VoidCallback onTap,
Color? color,
}) {
return Row(
children: [
MyText.bodyMedium(label, fontWeight: 600),
Tooltip(
message: tooltip,
child: InkWell(
borderRadius: BorderRadius.circular(22),
onTap: onTap,
child: MouseRegion(
cursor: SystemMouseCursors.click,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Icon(icon, color: color, size: 22),
),
),
),
),
],
);
}
Future<void> _openFilterSheet() async {
final result = await showModalBottomSheet<Map<String, dynamic>>(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (context) => DailyProgressReportFilter(
controller: dailyTaskController,
permissionController: permissionController,
),
);
if (result != null) {
final selectedProjectId = result['projectId'] as String?;
if (selectedProjectId != null &&
selectedProjectId != dailyTaskController.selectedProjectId) {
dailyTaskController.selectedProjectId = selectedProjectId;
await dailyTaskController.fetchTaskData(selectedProjectId);
dailyTaskController.update(['daily_progress_report_controller']);
}
}
}
Future<void> _refreshData() async {
final projectId = dailyTaskController.selectedProjectId;
if (projectId != null) {
try {
await dailyTaskController.fetchTaskData(projectId);
} catch (e) {
debugPrint('Error refreshing task data: $e');
}
}
}
void _showTeamMembersBottomSheet(List<dynamic> members) {
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
isDismissible: true,
enableDrag: true,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(12)),
),
builder: (context) {
return GestureDetector(
onTap: () {},
child: Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius:
const BorderRadius.vertical(top: Radius.circular(12)),
),
padding: const EdgeInsets.fromLTRB(16, 24, 16, 24),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MyText.titleMedium(
'Team Members',
fontWeight: 600,
),
const SizedBox(height: 8),
const Divider(thickness: 1),
const SizedBox(height: 8),
...members.map((member) {
final firstName = member.firstName ?? 'Unnamed';
final lastName = member.lastName ?? 'User';
return ListTile(
contentPadding: EdgeInsets.zero,
leading: Avatar(
firstName: firstName,
lastName: lastName,
size: 31,
),
title: MyText.bodyMedium(
'$firstName $lastName',
fontWeight: 600,
),
);
}),
const SizedBox(height: 8),
],
),
),
);
},
);
}
Widget _buildDailyProgressReportTab() {
return Obx(() {
final isLoading = dailyTaskController.isLoading.value;
final groupedTasks = dailyTaskController.groupedDailyTasks;
// Initial loading skeleton
if (isLoading && dailyTaskController.currentPage == 1) {
return SkeletonLoaders.dailyProgressReportSkeletonLoader();
}
// No tasks
if (groupedTasks.isEmpty) {
return Center(
child: MyText.bodySmall(
"No Progress Report Found",
fontWeight: 600,
),
);
}
final sortedDates = groupedTasks.keys.toList()
..sort((a, b) => b.compareTo(a));
// If only one date, make it expanded by default
if (sortedDates.length == 1 &&
!dailyTaskController.expandedDates.contains(sortedDates[0])) {
dailyTaskController.expandedDates.add(sortedDates[0]);
}
return MyCard.bordered(
borderRadiusAll: 10,
border: Border.all(color: Colors.grey.withOpacity(0.2)),
shadow: MyShadow(elevation: 1, position: MyShadowPosition.bottom),
paddingAll: 8,
child: ListView.builder(
controller: _scrollController,
shrinkWrap: true,
physics: const AlwaysScrollableScrollPhysics(),
itemCount: sortedDates.length + 1, // +1 for loading indicator
itemBuilder: (context, dateIndex) {
// Bottom loading indicator
if (dateIndex == sortedDates.length) {
return Obx(() => dailyTaskController.isLoadingMore.value
? const Padding(
padding: EdgeInsets.all(16.0),
child: Center(child: CircularProgressIndicator()),
)
: const SizedBox.shrink());
}
final dateKey = sortedDates[dateIndex];
final tasksForDate = groupedTasks[dateKey]!;
final date = DateTime.tryParse(dateKey);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
GestureDetector(
onTap: () => dailyTaskController.toggleDate(dateKey),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
MyText.bodyMedium(
date != null
? DateFormat('dd MMM yyyy').format(date)
: dateKey,
fontWeight: 700,
),
Obx(() => Icon(
dailyTaskController.expandedDates.contains(dateKey)
? Icons.remove_circle
: Icons.add_circle,
color: Colors.blueAccent,
)),
],
),
),
Obx(() {
if (!dailyTaskController.expandedDates.contains(dateKey)) {
return const SizedBox.shrink();
}
return Column(
children: tasksForDate.asMap().entries.map((entry) {
final task = entry.value;
final activityName =
task.workItem?.activityMaster?.activityName ?? 'N/A';
final activityId = task.workItem?.activityMaster?.id;
final workAreaId = task.workItem?.workArea?.id;
final location = [
task.workItem?.workArea?.floor?.building?.name,
task.workItem?.workArea?.floor?.floorName,
task.workItem?.workArea?.areaName
].where((e) => e?.isNotEmpty ?? false).join(' > ');
final planned = task.plannedTask;
final completed = task.completedTask;
final progress = (planned != 0)
? (completed / planned).clamp(0.0, 1.0)
: 0.0;
final parentTaskID = task.id;
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: MyContainer(
paddingAll: 12,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MyText.bodyMedium(activityName, fontWeight: 600),
const SizedBox(height: 2),
MyText.bodySmall(location, color: Colors.grey),
const SizedBox(height: 8),
GestureDetector(
onTap: () => _showTeamMembersBottomSheet(
task.teamMembers),
child: Row(
children: [
const Icon(Icons.group,
size: 18, color: Colors.blueAccent),
const SizedBox(width: 6),
MyText.bodyMedium('Team',
color: Colors.blueAccent,
fontWeight: 600),
],
),
),
const SizedBox(height: 8),
MyText.bodySmall(
"Completed: $completed / $planned",
fontWeight: 600,
color: Colors.black87,
),
const SizedBox(height: 6),
Stack(
children: [
Container(
height: 5,
decoration: BoxDecoration(
color: Colors.grey[300],
borderRadius: BorderRadius.circular(6),
),
),
FractionallySizedBox(
widthFactor: progress,
child: Container(
height: 5,
decoration: BoxDecoration(
color: progress >= 1.0
? Colors.green
: progress >= 0.5
? Colors.amber
: Colors.red,
borderRadius: BorderRadius.circular(6),
),
),
),
],
),
const SizedBox(height: 4),
MyText.bodySmall(
"${(progress * 100).toStringAsFixed(1)}%",
fontWeight: 500,
color: progress >= 1.0
? Colors.green[700]
: progress >= 0.5
? Colors.amber[800]
: Colors.red[700],
),
const SizedBox(height: 12),
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
if ((task.reportedDate == null ||
task.reportedDate
.toString()
.isEmpty) &&
permissionController.hasPermission(
Permissions.assignReportTask)) ...[
TaskActionButtons.reportButton(
context: context,
task: task,
completed: completed.toInt(),
refreshCallback: _refreshData,
),
const SizedBox(width: 4),
] else if (task.approvedBy == null &&
permissionController.hasPermission(
Permissions.approveTask)) ...[
TaskActionButtons.reportActionButton(
context: context,
task: task,
parentTaskID: parentTaskID,
workAreaId: workAreaId.toString(),
activityId: activityId.toString(),
completed: completed.toInt(),
refreshCallback: _refreshData,
),
const SizedBox(width: 5),
],
TaskActionButtons.commentButton(
context: context,
task: task,
parentTaskID: parentTaskID,
workAreaId: workAreaId.toString(),
activityId: activityId.toString(),
refreshCallback: _refreshData,
),
],
),
),
],
),
),
);
}).toList(),
);
})
],
);
},
),
);
});
}
}

View File

@ -1,531 +0,0 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:marco/helpers/theme/app_theme.dart';
import 'package:marco/helpers/utils/mixins/ui_mixin.dart';
import 'package:marco/helpers/widgets/my_card.dart';
import 'package:marco/helpers/widgets/my_spacing.dart';
import 'package:marco/helpers/widgets/my_text.dart';
import 'package:marco/controller/permission_controller.dart';
import 'package:marco/controller/task_Planning/daily_task_Planning_controller.dart';
import 'package:marco/controller/project_controller.dart';
import 'package:percent_indicator/percent_indicator.dart';
import 'package:marco/model/dailyTaskPlanning/assign_task_bottom_sheet .dart';
import 'package:marco/helpers/widgets/my_custom_skeleton.dart';
import 'package:marco/helpers/utils/permission_constants.dart';
import 'package:marco/helpers/widgets/my_refresh_indicator.dart';
import 'package:marco/controller/tenant/service_controller.dart';
import 'package:marco/helpers/widgets/tenant/service_selector.dart';
class DailyTaskPlanningScreen extends StatefulWidget {
DailyTaskPlanningScreen({super.key});
@override
State<DailyTaskPlanningScreen> createState() =>
_DailyTaskPlanningScreenState();
}
class _DailyTaskPlanningScreenState extends State<DailyTaskPlanningScreen>
with UIMixin {
final DailyTaskPlanningController dailyTaskPlanningController =
Get.put(DailyTaskPlanningController());
final PermissionController permissionController =
Get.put(PermissionController());
final ProjectController projectController = Get.find<ProjectController>();
final ServiceController serviceController = Get.put(ServiceController());
@override
void initState() {
super.initState();
final projectId = projectController.selectedProjectId.value;
if (projectId.isNotEmpty) {
dailyTaskPlanningController.fetchTaskData(projectId);
serviceController.fetchServices(projectId); // <-- Fetch services here
}
ever<String>(
projectController.selectedProjectId,
(newProjectId) {
if (newProjectId.isNotEmpty) {
dailyTaskPlanningController.fetchTaskData(newProjectId);
serviceController
.fetchServices(newProjectId);
}
},
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: PreferredSize(
preferredSize: const Size.fromHeight(72),
child: AppBar(
backgroundColor: const Color(0xFFF5F5F5),
elevation: 0.5,
automaticallyImplyLeading: false,
titleSpacing: 0,
title: Padding(
padding: MySpacing.xy(16, 0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
IconButton(
icon: const Icon(Icons.arrow_back_ios_new,
color: Colors.black, size: 20),
onPressed: () => Get.offNamed('/dashboard'),
),
MySpacing.width(8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
MyText.titleLarge(
'Daily Task Planning',
fontWeight: 700,
color: Colors.black,
),
MySpacing.height(2),
GetBuilder<ProjectController>(
builder: (projectController) {
final projectName =
projectController.selectedProject?.name ??
'Select Project';
return Row(
children: [
const Icon(Icons.work_outline,
size: 14, color: Colors.grey),
MySpacing.width(4),
Expanded(
child: MyText.bodySmall(
projectName,
fontWeight: 600,
overflow: TextOverflow.ellipsis,
color: Colors.grey[700],
),
),
],
);
},
),
],
),
),
],
),
),
),
),
body: SafeArea(
child: MyRefreshIndicator(
onRefresh: () async {
final projectId = projectController.selectedProjectId.value;
if (projectId.isNotEmpty) {
try {
await dailyTaskPlanningController.fetchTaskData(projectId);
} catch (e) {
debugPrint('Error refreshing task data: ${e.toString()}');
}
}
},
child: SingleChildScrollView(
physics:
const AlwaysScrollableScrollPhysics(), // <-- always allow drag
padding: MySpacing.x(0),
child: ConstrainedBox(
// <-- ensures full screen height
constraints: BoxConstraints(
minHeight: MediaQuery.of(context).size.height -
kToolbarHeight -
MediaQuery.of(context).padding.top,
),
child: GetBuilder<DailyTaskPlanningController>(
init: dailyTaskPlanningController,
tag: 'daily_task_Planning_controller',
builder: (controller) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MySpacing.height(flexSpacing),
Padding(
padding: MySpacing.x(10),
child: ServiceSelector(
controller: serviceController,
height: 40,
onSelectionChanged: (service) async {
final projectId =
projectController.selectedProjectId.value;
if (projectId.isNotEmpty) {
await dailyTaskPlanningController.fetchTaskData(
projectId,
// serviceId: service
// ?.id,
);
}
},
),
),
MySpacing.height(flexSpacing),
Padding(
padding: MySpacing.x(8),
child: dailyProgressReportTab(),
),
],
);
},
),
),
),
),
),
);
}
Widget dailyProgressReportTab() {
return Obx(() {
final isLoading = dailyTaskPlanningController.isLoading.value;
final dailyTasks = dailyTaskPlanningController.dailyTasks;
if (isLoading) {
return SkeletonLoaders.dailyProgressPlanningSkeletonCollapsedOnly();
}
if (dailyTasks.isEmpty) {
return Center(
child: MyText.bodySmall(
"No Progress Report Found",
fontWeight: 600,
),
);
}
final buildingExpansionState = <String, bool>{};
final floorExpansionState = <String, bool>{};
Widget buildExpandIcon(bool isExpanded) {
return Container(
width: 32,
height: 32,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.grey.shade200,
),
child: Icon(
isExpanded ? Icons.remove : Icons.add,
size: 20,
color: Colors.black87,
),
);
}
return StatefulBuilder(builder: (context, setMainState) {
final filteredBuildings = dailyTasks.expand((task) {
return task.buildings.where((building) {
return building.floors.any((floor) =>
floor.workAreas.any((area) => area.workItems.isNotEmpty));
});
}).toList();
if (filteredBuildings.isEmpty) {
return Center(
child: MyText.bodySmall(
"No Progress Report Found",
fontWeight: 600,
),
);
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: filteredBuildings.map((building) {
final buildingKey = building.id.toString();
return MyCard.bordered(
borderRadiusAll: 10,
paddingAll: 0,
margin: MySpacing.bottom(10),
child: Theme(
data: Theme.of(context)
.copyWith(dividerColor: Colors.transparent),
child: ExpansionTile(
onExpansionChanged: (expanded) {
setMainState(() {
buildingExpansionState[buildingKey] = expanded;
});
},
trailing: buildExpandIcon(
buildingExpansionState[buildingKey] ?? false),
tilePadding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 0),
collapsedShape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
leading: Container(
decoration: BoxDecoration(
color: Colors.blueAccent.withOpacity(0.1),
shape: BoxShape.circle,
),
padding: const EdgeInsets.all(8),
child: Icon(
Icons.location_city_rounded,
color: Colors.blueAccent,
size: 24,
),
),
title: MyText.titleMedium(
building.name,
fontWeight: 700,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
childrenPadding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 0),
children: building.floors.expand((floor) {
final validWorkAreas = floor.workAreas
.where((area) => area.workItems.isNotEmpty);
// For each valid work area, return a Floor+WorkArea ExpansionTile
return validWorkAreas.map((area) {
final floorWorkAreaKey =
"${buildingKey}_${floor.floorName}_${area.areaName}";
final isExpanded =
floorExpansionState[floorWorkAreaKey] ?? false;
final workItems = area.workItems;
final totalPlanned = workItems.fold<double>(
0, (sum, wi) => sum + (wi.workItem.plannedWork ?? 0));
final totalCompleted = workItems.fold<double>(0,
(sum, wi) => sum + (wi.workItem.completedWork ?? 0));
final totalProgress = totalPlanned == 0
? 0.0
: (totalCompleted / totalPlanned).clamp(0.0, 1.0);
return ExpansionTile(
onExpansionChanged: (expanded) {
setMainState(() {
floorExpansionState[floorWorkAreaKey] = expanded;
});
},
trailing: Icon(
isExpanded
? Icons.keyboard_arrow_up
: Icons.keyboard_arrow_down,
size: 28,
color: Colors.black54,
),
tilePadding: const EdgeInsets.symmetric(
horizontal: 16, vertical: 0),
title: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
flex: 3,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MyText.titleSmall(
"Floor: ${floor.floorName}",
fontWeight: 600,
color: Colors.teal,
maxLines: null,
overflow: TextOverflow.visible,
softWrap: true,
),
MySpacing.height(4),
MyText.titleSmall(
"Work Area: ${area.areaName}",
fontWeight: 600,
color: Colors.blueGrey,
maxLines: null,
overflow: TextOverflow.visible,
softWrap: true,
),
],
),
),
MySpacing.width(12),
CircularPercentIndicator(
radius: 20.0,
lineWidth: 4.0,
animation: true,
percent: totalProgress,
center: Text(
"${(totalProgress * 100).toStringAsFixed(0)}%",
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 10.0,
),
),
circularStrokeCap: CircularStrokeCap.round,
progressColor: totalProgress >= 1.0
? Colors.green
: (totalProgress >= 0.5
? Colors.amber
: Colors.red),
backgroundColor: Colors.grey[300]!,
),
],
),
childrenPadding: const EdgeInsets.only(
left: 16, right: 0, bottom: 8),
children: area.workItems.map((wItem) {
final item = wItem.workItem;
final completed = item.completedWork ?? 0;
final planned = item.plannedWork ?? 0;
final progress = (planned == 0)
? 0.0
: (completed / planned).clamp(0.0, 1.0);
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: MyText.bodyMedium(
item.activityMaster?.name ??
"No Activity",
fontWeight: 600,
maxLines: 2,
overflow: TextOverflow.visible,
softWrap: true,
),
),
MySpacing.width(8),
if (item.workCategoryMaster?.name != null)
Container(
padding: const EdgeInsets.symmetric(
horizontal: 12, vertical: 4),
decoration: BoxDecoration(
color: Colors.blue.shade100,
borderRadius:
BorderRadius.circular(20),
),
child: MyText.bodySmall(
item.workCategoryMaster!.name!,
fontWeight: 500,
color: Colors.blue.shade800,
),
),
],
),
MySpacing.height(4),
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
flex: 3,
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
MySpacing.height(8),
MyText.bodySmall(
"Completed: $completed / $planned",
fontWeight: 600,
color: const Color.fromARGB(
221, 0, 0, 0),
),
],
),
),
MySpacing.width(16),
if (progress < 1.0 &&
permissionController.hasPermission(
Permissions.assignReportTask))
IconButton(
icon: Icon(
Icons.person_add_alt_1_rounded,
color:
Color.fromARGB(255, 46, 161, 233),
),
onPressed: () {
final pendingTask =
(planned - completed)
.clamp(0, planned)
.toInt();
showModalBottomSheet(
context: context,
isScrollControlled: true,
shape: const RoundedRectangleBorder(
borderRadius:
BorderRadius.vertical(
top: Radius.circular(16)),
),
builder: (context) =>
AssignTaskBottomSheet(
buildingName: building.name,
floorName: floor.floorName,
workAreaName: area.areaName,
workLocation: area.areaName,
activityName:
item.activityMaster?.name ??
"Unknown Activity",
pendingTask: pendingTask,
workItemId: item.id.toString(),
assignmentDate: DateTime.now(),
),
);
},
),
],
),
MySpacing.height(8),
Stack(
children: [
Container(
height: 5,
decoration: BoxDecoration(
color: Colors.grey[300],
borderRadius: BorderRadius.circular(6),
),
),
FractionallySizedBox(
widthFactor: progress,
child: Container(
height: 5,
decoration: BoxDecoration(
color: progress >= 1.0
? Colors.green
: (progress >= 0.5
? Colors.amber
: Colors.red),
borderRadius:
BorderRadius.circular(6),
),
),
),
],
),
SizedBox(height: 4),
MyText.bodySmall(
"${(progress * 100).toStringAsFixed(1)}%",
fontWeight: 500,
color: progress >= 1.0
? Colors.green[700]
: (progress >= 0.5
? Colors.amber[800]
: Colors.red[700]),
),
],
),
);
}).toList(),
);
}).toList();
}).toList(),
),
),
);
}).toList(),
);
});
});
}
}

View File

@ -1,391 +0,0 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:marco/helpers/services/api_endpoints.dart';
import 'package:marco/helpers/utils/mixins/ui_mixin.dart';
import 'package:marco/helpers/widgets/my_text.dart';
import 'package:marco/images.dart';
import 'package:marco/controller/tenant/tenant_selection_controller.dart';
class TenantSelectionScreen extends StatefulWidget {
const TenantSelectionScreen({super.key});
@override
State<TenantSelectionScreen> createState() => _TenantSelectionScreenState();
}
class _TenantSelectionScreenState extends State<TenantSelectionScreen>
with UIMixin, SingleTickerProviderStateMixin {
late final TenantSelectionController _controller;
late final AnimationController _logoAnimController;
late final Animation<double> _logoAnimation;
final bool _isBetaEnvironment = ApiEndpoints.baseUrl.contains("stage");
bool _isLoading = false;
@override
void initState() {
super.initState();
_controller = Get.put(TenantSelectionController());
_logoAnimController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 800),
);
_logoAnimation = CurvedAnimation(
parent: _logoAnimController,
curve: Curves.easeOutBack,
);
_logoAnimController.forward();
// 🔥 Tell controller this is tenant selection screen
_controller.loadTenants(fromTenantSelectionScreen: true);
}
@override
void dispose() {
_logoAnimController.dispose();
Get.delete<TenantSelectionController>();
super.dispose();
}
Future<void> _onTenantSelected(String tenantId) async {
setState(() => _isLoading = true);
await _controller.onTenantSelected(tenantId);
setState(() => _isLoading = false);
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Stack(
children: [
_RedWaveBackground(brandRed: contentTheme.brandRed),
SafeArea(
child: Center(
child: Column(
children: [
const SizedBox(height: 24),
_AnimatedLogo(animation: _logoAnimation),
const SizedBox(height: 8),
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 420),
child: Column(
children: [
const SizedBox(height: 12),
const _WelcomeTexts(),
if (_isBetaEnvironment) ...[
const SizedBox(height: 12),
const _BetaBadge(),
],
const SizedBox(height: 36),
// Tenant list directly reacts to controller
TenantCardList(
controller: _controller,
isLoading: _isLoading,
onTenantSelected: _onTenantSelected,
),
],
),
),
),
),
),
],
),
),
),
],
),
);
}
}
class _AnimatedLogo extends StatelessWidget {
final Animation<double> animation;
const _AnimatedLogo({required this.animation});
@override
Widget build(BuildContext context) {
return ScaleTransition(
scale: animation,
child: Container(
width: 100,
height: 100,
padding: const EdgeInsets.all(20),
decoration: const BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Colors.black12,
blurRadius: 10,
offset: Offset(0, 4),
),
],
),
child: Image.asset(Images.logoDark),
),
);
}
}
class _WelcomeTexts extends StatelessWidget {
const _WelcomeTexts();
@override
Widget build(BuildContext context) {
return Column(
children: [
MyText(
"Welcome",
fontSize: 24,
fontWeight: 600,
color: Colors.black87,
textAlign: TextAlign.center,
),
const SizedBox(height: 10),
MyText(
"Please select which dashboard you want to explore!.",
fontSize: 14,
color: Colors.black54,
textAlign: TextAlign.center,
),
],
);
}
}
class _BetaBadge extends StatelessWidget {
const _BetaBadge();
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: Colors.orangeAccent,
borderRadius: BorderRadius.circular(5),
),
child: MyText(
'BETA',
color: Colors.white,
fontWeight: 600,
fontSize: 12,
),
);
}
}
class TenantCardList extends StatelessWidget {
final TenantSelectionController controller;
final bool isLoading;
final Function(String tenantId) onTenantSelected;
const TenantCardList({
required this.controller,
required this.isLoading,
required this.onTenantSelected,
});
@override
Widget build(BuildContext context) {
return Obx(() {
if (controller.isLoading.value || isLoading) {
return const Center(
child: CircularProgressIndicator(strokeWidth: 2),
);
}
if (controller.tenants.isEmpty) {
return Center(
child: MyText(
"No dashboards available for your account.",
fontSize: 14,
color: Colors.black54,
textAlign: TextAlign.center,
),
);
}
if (controller.tenants.length == 1) {
return const SizedBox.shrink();
}
return SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 0, vertical: 24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
...controller.tenants.map(
(tenant) => _TenantCard(
tenant: tenant,
onTap: () => onTenantSelected(tenant.id),
),
),
const SizedBox(height: 16),
TextButton.icon(
onPressed: () => Get.back(),
icon: const Icon(Icons.arrow_back,
size: 20, color: Colors.redAccent),
label: MyText(
'Back to Login',
color: Colors.red,
fontWeight: 600,
fontSize: 14,
),
),
],
),
);
});
}
}
class _TenantCard extends StatelessWidget {
final dynamic tenant;
final VoidCallback onTap;
const _TenantCard({required this.tenant, required this.onTap});
@override
Widget build(BuildContext context) {
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(5),
child: Card(
elevation: 3,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(5),
),
margin: const EdgeInsets.only(bottom: 20),
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ClipRRect(
borderRadius: BorderRadius.circular(5),
child: Container(
width: 60,
height: 60,
color: Colors.grey.shade200,
child: TenantLogo(logoImage: tenant.logoImage),
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MyText(
tenant.name,
fontSize: 18,
fontWeight: 700,
color: Colors.black87,
),
const SizedBox(height: 6),
MyText(
"Industry: ${tenant.industry?.name ?? "-"}",
fontSize: 13,
color: Colors.black54,
),
],
),
),
Icon(
Icons.arrow_forward_ios,
size: 24,
color: Colors.red,
),
],
),
),
),
);
}
}
class TenantLogo extends StatelessWidget {
final String? logoImage;
const TenantLogo({required this.logoImage});
@override
Widget build(BuildContext context) {
if (logoImage == null || logoImage!.isEmpty) {
return Center(
child: Icon(Icons.business, color: Colors.grey.shade600),
);
}
if (logoImage!.startsWith("data:image")) {
try {
final base64Str = logoImage!.split(',').last;
final bytes = base64Decode(base64Str);
return Image.memory(bytes, fit: BoxFit.cover);
} catch (_) {
return Center(
child: Icon(Icons.business, color: Colors.grey.shade600),
);
}
} else {
return Image.network(
logoImage!,
fit: BoxFit.cover,
errorBuilder: (_, __, ___) => Center(
child: Icon(Icons.business, color: Colors.grey.shade600),
),
);
}
}
}
class _RedWaveBackground extends StatelessWidget {
final Color brandRed;
const _RedWaveBackground({required this.brandRed});
@override
Widget build(BuildContext context) {
return CustomPaint(
painter: _WavePainter(brandRed),
size: Size.infinite,
);
}
}
class _WavePainter extends CustomPainter {
final Color brandRed;
_WavePainter(this.brandRed);
@override
void paint(Canvas canvas, Size size) {
final paint1 = Paint()
..shader = LinearGradient(
colors: [brandRed, const Color.fromARGB(255, 97, 22, 22)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
).createShader(Rect.fromLTWH(0, 0, size.width, size.height));
final path1 = Path()
..moveTo(0, size.height * 0.2)
..quadraticBezierTo(size.width * 0.25, size.height * 0.05,
size.width * 0.5, size.height * 0.15)
..quadraticBezierTo(
size.width * 0.75, size.height * 0.25, size.width, size.height * 0.1)
..lineTo(size.width, 0)
..lineTo(0, 0)
..close();
canvas.drawPath(path1, paint1);
final paint2 = Paint()..color = Colors.redAccent.withOpacity(0.15);
final path2 = Path()
..moveTo(0, size.height * 0.25)
..quadraticBezierTo(
size.width * 0.4, size.height * 0.1, size.width, size.height * 0.2)
..lineTo(size.width, 0)
..lineTo(0, 0)
..close();
canvas.drawPath(path2, paint2);
}
@override
bool shouldRepaint(CustomPainter oldDelegate) => false;
}