From 4feb2875f0d5e44f207daf3d068162f850eeb873 Mon Sep 17 00:00:00 2001 From: Vaibhav Surve Date: Tue, 30 Sep 2025 17:00:20 +0530 Subject: [PATCH] removed unwanted files --- .../task_planning/add_task_controller.dart | 152 ---- .../task_planning/daily_task_controller.dart | 146 ---- .../daily_task_planning_controller.dart | 279 ------- .../report_task_action_controller.dart | 296 -------- .../task_planning/report_task_controller.dart | 248 ------- .../organization_selection_controller.dart | 52 -- lib/controller/tenant/service_controller.dart | 43 -- .../tenant/tenant_selection_controller.dart | 106 --- lib/helpers/services/api_service.dart | 31 - .../services/notification_action_handler.dart | 51 -- lib/helpers/services/tenant_service.dart | 152 ---- .../widgets/tenant/organization_selector.dart | 106 --- .../widgets/tenant/service_selector.dart | 114 --- .../assign_task_bottom_sheet .dart | 368 ---------- .../comment_task_bottom_sheet.dart | 678 ------------------ .../create_task_botom_sheet.dart | 213 ------ .../daily_progress_report_filter.dart | 83 --- .../dailyTaskPlanning/daily_task_model.dart | 222 ------ .../daily_task_planning_filter.dart | 144 ---- .../daily_task_planning_model.dart | 246 ------- .../master_work_category_model.dart | 31 - .../report_action_bottom_sheet.dart | 500 ------------- .../report_action_widgets.dart | 392 ---------- .../report_task_bottom_sheet.dart | 310 -------- .../task_action_buttons.dart | 210 ------ .../dailyTaskPlanning/task_list_model.dart | 45 -- .../dailyTaskPlanning/work_status_model.dart | 53 -- lib/model/tenant/tenant_list_model.dart | 109 --- lib/model/tenant/tenant_services_model.dart | 78 -- lib/routes.dart | 12 +- lib/view/taskPlanning/daily_progress.dart | 572 --------------- .../taskPlanning/daily_task_planning.dart | 531 -------------- lib/view/tenant/tenant_selection_screen.dart | 391 ---------- 33 files changed, 1 insertion(+), 6963 deletions(-) delete mode 100644 lib/controller/task_planning/add_task_controller.dart delete mode 100644 lib/controller/task_planning/daily_task_controller.dart delete mode 100644 lib/controller/task_planning/daily_task_planning_controller.dart delete mode 100644 lib/controller/task_planning/report_task_action_controller.dart delete mode 100644 lib/controller/task_planning/report_task_controller.dart delete mode 100644 lib/controller/tenant/organization_selection_controller.dart delete mode 100644 lib/controller/tenant/service_controller.dart delete mode 100644 lib/controller/tenant/tenant_selection_controller.dart delete mode 100644 lib/helpers/services/tenant_service.dart delete mode 100644 lib/helpers/widgets/tenant/organization_selector.dart delete mode 100644 lib/helpers/widgets/tenant/service_selector.dart delete mode 100644 lib/model/dailyTaskPlanning/assign_task_bottom_sheet .dart delete mode 100644 lib/model/dailyTaskPlanning/comment_task_bottom_sheet.dart delete mode 100644 lib/model/dailyTaskPlanning/create_task_botom_sheet.dart delete mode 100644 lib/model/dailyTaskPlanning/daily_progress_report_filter.dart delete mode 100644 lib/model/dailyTaskPlanning/daily_task_model.dart delete mode 100644 lib/model/dailyTaskPlanning/daily_task_planning_filter.dart delete mode 100644 lib/model/dailyTaskPlanning/daily_task_planning_model.dart delete mode 100644 lib/model/dailyTaskPlanning/master_work_category_model.dart delete mode 100644 lib/model/dailyTaskPlanning/report_action_bottom_sheet.dart delete mode 100644 lib/model/dailyTaskPlanning/report_action_widgets.dart delete mode 100644 lib/model/dailyTaskPlanning/report_task_bottom_sheet.dart delete mode 100644 lib/model/dailyTaskPlanning/task_action_buttons.dart delete mode 100644 lib/model/dailyTaskPlanning/task_list_model.dart delete mode 100644 lib/model/dailyTaskPlanning/work_status_model.dart delete mode 100644 lib/model/tenant/tenant_list_model.dart delete mode 100644 lib/model/tenant/tenant_services_model.dart delete mode 100644 lib/view/taskPlanning/daily_progress.dart delete mode 100644 lib/view/taskPlanning/daily_task_planning.dart delete mode 100644 lib/view/tenant/tenant_selection_screen.dart diff --git a/lib/controller/task_planning/add_task_controller.dart b/lib/controller/task_planning/add_task_controller.dart deleted file mode 100644 index b3df752..0000000 --- a/lib/controller/task_planning/add_task_controller.dart +++ /dev/null @@ -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 uploadingStates = {}.obs; - MyFormValidator basicValidator = MyFormValidator(); - RxnString selectedCategoryId = RxnString(); - RxnString selectedCategoryName = RxnString(); - var categoryIdNameMap = {}.obs; - - List> roles = []; - RxnString selectedRoleId = RxnString(); - RxBool isLoadingWorkMasterCategories = false.obs; - RxList workMasterCategories = [].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 assignDailyTask({ - required String workItemId, - required int plannedTask, - required String description, - required List 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 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 fetchWorkMasterCategories() async { - isLoadingWorkMasterCategories.value = true; - - try { - final response = await ApiService.getMasterWorkCategories(); - if (response != null) { - final dataList = response['data'] ?? []; - - final parsedList = List.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, ); - } -} diff --git a/lib/controller/task_planning/daily_task_controller.dart b/lib/controller/task_planning/daily_task_controller.dart deleted file mode 100644 index 644bc9f..0000000 --- a/lib/controller/task_planning/daily_task_controller.dart +++ /dev/null @@ -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 projects = []; - String? selectedProjectId; - - DateTime? startDateTask; - DateTime? endDateTask; - - List dailyTasks = []; - final RxSet expandedDates = {}.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> 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 fetchTaskData( - String projectId, { - List? 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 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 - } -} diff --git a/lib/controller/task_planning/daily_task_planning_controller.dart b/lib/controller/task_planning/daily_task_planning_controller.dart deleted file mode 100644 index f94af8b..0000000 --- a/lib/controller/task_planning/daily_task_planning_controller.dart +++ /dev/null @@ -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 projects = []; - List employees = []; - List dailyTasks = []; - - RxMap uploadingStates = {}.obs; - RxList selectedEmployees = [].obs; - - MyFormValidator basicValidator = MyFormValidator(); - List> 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 fetchRoles() async { - logSafe("Fetching roles...", level: LogLevel.info); - final result = await ApiService.getRoles(); - if (result != null) { - roles = List>.from(result); - logSafe("Roles fetched successfully", level: LogLevel.info); - update(); - } else { - logSafe("Failed to fetch roles", level: LogLevel.error); - } - } - - Future assignDailyTask({ - required String workItemId, - required int plannedTask, - required String description, - required List 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 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 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?; - - 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).map((floorJson) { - return Floor( - id: floorJson['id'], - floorName: floorJson['floorName'], - workAreas: - (floorJson['workAreas'] as List).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? ?? []; - - 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 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(); - } - } -} diff --git a/lib/controller/task_planning/report_task_action_controller.dart b/lib/controller/task_planning/report_task_action_controller.dart deleted file mode 100644 index 9722dd0..0000000 --- a/lib/controller/task_planning/report_task_action_controller.dart +++ /dev/null @@ -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 reportStatus = ApiStatus.idle.obs; - final Rx commentStatus = ApiStatus.idle.obs; - - final RxList selectedImages = [].obs; - final RxList workStatus = [].obs; - final RxList workStatuses = [].obs; - - final RxBool showAddTaskCheckbox = false.obs; - final RxBool isAddTaskChecked = false.obs; - - final RxBool isLoadingWorkStatus = false.obs; - final Rxn> selectedTask = Rxn>(); - - 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 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 approveTask({ - required String projectId, - required String comment, - required String reportActionId, - required String approvedTaskCount, - List? 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 commentTask({ - required String projectId, - required String comment, - List? 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 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>?> _prepareImages(List? 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>().length} images."); - return results.whereType>().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 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); -} diff --git a/lib/controller/task_planning/report_task_controller.dart b/lib/controller/task_planning/report_task_controller.dart deleted file mode 100644 index 5956b4b..0000000 --- a/lib/controller/task_planning/report_task_controller.dart +++ /dev/null @@ -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 files = []; - MyFormValidator basicValidator = MyFormValidator(); - RxBool isLoading = false.obs; - Rx reportStatus = ApiStatus.idle.obs; - Rx commentStatus = ApiStatus.idle.obs; - - RxList selectedImages = [].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 reportTask({ - required String projectId, - required String comment, - required int completedTask, - required List> checklist, - required DateTime reportedDate, - List? 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 commentTask({ - required String projectId, - required String comment, - List? 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>?> _prepareImages(List? 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>().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 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, - ); -} diff --git a/lib/controller/tenant/organization_selection_controller.dart b/lib/controller/tenant/organization_selection_controller.dart deleted file mode 100644 index ee6db6e..0000000 --- a/lib/controller/tenant/organization_selection_controller.dart +++ /dev/null @@ -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 organizations = []; - - /// Currently selected organization (reactive) - Rxn selectedOrganization = Rxn(); - - /// Loading state for fetching organizations - final isLoadingOrganizations = false.obs; - - /// Fetch organizations assigned to a given project - Future 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"; -} diff --git a/lib/controller/tenant/service_controller.dart b/lib/controller/tenant/service_controller.dart deleted file mode 100644 index f832157..0000000 --- a/lib/controller/tenant/service_controller.dart +++ /dev/null @@ -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 services = []; - Service? selectedService; - final isLoadingServices = false.obs; - - /// Fetch services assigned to a project - Future 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"; -} diff --git a/lib/controller/tenant/tenant_selection_controller.dart b/lib/controller/tenant/tenant_selection_controller.dart deleted file mode 100644 index 7789de9..0000000 --- a/lib/controller/tenant/tenant_selection_controller.dart +++ /dev/null @@ -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 = [].obs; - var isLoading = false.obs; - - @override - void onInit() { - super.onInit(); - loadTenants(); - } - - /// Load tenants from API - Future 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 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; - } - } -} diff --git a/lib/helpers/services/api_service.dart b/lib/helpers/services/api_service.dart index 1f15ef4..924fa6c 100644 --- a/lib/helpers/services/api_service.dart +++ b/lib/helpers/services/api_service.dart @@ -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 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 postLogsApi(List> logs) async { const endpoint = "${ApiEndpoints.uploadLogs}"; logSafe("Posting logs... count=${logs.length}"); diff --git a/lib/helpers/services/notification_action_handler.dart b/lib/helpers/services/notification_action_handler.dart index d21cb32..3f8a54a 100644 --- a/lib/helpers/services/notification_action_handler.dart +++ b/lib/helpers/services/notification_action_handler.dart @@ -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 data) { - final projectId = data['ProjectId']; - if (projectId == null) { - _logger.w("⚠️ TaskPlanning update received without ProjectId: $data"); - return; - } - _safeControllerUpdate( - 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 data, - {required bool isComment}) { - _safeControllerUpdate( - 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 data) { String entityTypeId; diff --git a/lib/helpers/services/tenant_service.dart b/lib/helpers/services/tenant_service.dart deleted file mode 100644 index 83acfc6..0000000 --- a/lib/helpers/services/tenant_service.dart +++ /dev/null @@ -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>?> getTenants({bool hasRetried = false}); - Future selectTenant(String tenantId, {bool hasRetried = false}); -} - -/// Tenant API service -class TenantService implements ITenantService { - static const String _baseUrl = ApiEndpoints.baseUrl; - static const Map _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> _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>?> 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>.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 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.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; - } - } -} diff --git a/lib/helpers/widgets/tenant/organization_selector.dart b/lib/helpers/widgets/tenant/organization_selector.dart deleted file mode 100644 index 8295f97..0000000 --- a/lib/helpers/widgets/tenant/organization_selector.dart +++ /dev/null @@ -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 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 items, - }) { - return PopupMenuButton( - 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(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, - ); - }); - } -} diff --git a/lib/helpers/widgets/tenant/service_selector.dart b/lib/helpers/widgets/tenant/service_selector.dart deleted file mode 100644 index 61b4ad4..0000000 --- a/lib/helpers/widgets/tenant/service_selector.dart +++ /dev/null @@ -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 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 items, - }) { - return PopupMenuButton( - 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( - enabled: false, - child: Center( - child: Text( - "No services found", - style: TextStyle(color: Colors.grey), - ), - ), - ), - ]; - } - return items - .map((e) => PopupMenuItem(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 - ? [] - : [ - "All Services", - ...controller.services.map((e) => e.name).toList(), - ]; - - final currentValue = - controller.services.isEmpty ? "" : controller.currentSelection; - - return _popupSelector( - currentValue: currentValue, - items: serviceNames, - ); - }); - } -} diff --git a/lib/model/dailyTaskPlanning/assign_task_bottom_sheet .dart b/lib/model/dailyTaskPlanning/assign_task_bottom_sheet .dart deleted file mode 100644 index b183f8d..0000000 --- a/lib/model/dailyTaskPlanning/assign_task_bottom_sheet .dart +++ /dev/null @@ -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 createState() => _AssignTaskBottomSheetState(); -} - -class _AssignTaskBottomSheetState extends State { - 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((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, - ); - } -} diff --git a/lib/model/dailyTaskPlanning/comment_task_bottom_sheet.dart b/lib/model/dailyTaskPlanning/comment_task_bottom_sheet.dart deleted file mode 100644 index 4184d49..0000000 --- a/lib/model/dailyTaskPlanning/comment_task_bottom_sheet.dart +++ /dev/null @@ -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 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 createState() => _CommentTaskBottomSheetState(); -} - -class _Member { - final String firstName; - _Member(this.firstName); -} - -class _CommentTaskBottomSheetState extends State - with UIMixin { - late final ReportTaskController controller; - List> _sortedComments = []; - - @override - void initState() { - super.initState(); - controller = Get.put(ReportTaskController(), - tag: widget.taskData['taskId'] ?? UniqueKey().toString()); - _initializeControllerData(); - - final comments = List>.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( - 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.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 sources, int initialIndex) { - showDialog( - context: context, - barrierColor: Colors.black87, - builder: (_) => ImageViewerDialog( - imageSources: sources, - initialIndex: initialIndex, - ), - ); - } - - Future _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 imageSources; // Can be List or List - 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 comment; - final String timeAgo; - final Function(List 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.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), - ), - ], - ], - ), - ); - } -} diff --git a/lib/model/dailyTaskPlanning/create_task_botom_sheet.dart b/lib/model/dailyTaskPlanning/create_task_botom_sheet.dart deleted file mode 100644 index 1d5fe62..0000000 --- a/lib/model/dailyTaskPlanning/create_task_botom_sheet.dart +++ /dev/null @@ -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( - padding: EdgeInsets.zero, - onSelected: (val) { - controller.selectCategory(val); - onCategoryChanged(val); - }, - itemBuilder: (context) => categoryMap.entries - .map((entry) => PopupMenuItem( - 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 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]), - ], - ), - ), - ], - ), - ); -} diff --git a/lib/model/dailyTaskPlanning/daily_progress_report_filter.dart b/lib/model/dailyTaskPlanning/daily_progress_report_filter.dart deleted file mode 100644 index 7a6744b..0000000 --- a/lib/model/dailyTaskPlanning/daily_progress_report_filter.dart +++ /dev/null @@ -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), - ], - ), - ), - ), - ], - ), - ); - } -} diff --git a/lib/model/dailyTaskPlanning/daily_task_model.dart b/lib/model/dailyTaskPlanning/daily_task_model.dart deleted file mode 100644 index 4c93b88..0000000 --- a/lib/model/dailyTaskPlanning/daily_task_model.dart +++ /dev/null @@ -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 teamMembers; - final List comments; - final List 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 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?) - ?.map((e) => e.toString()) - .toList() ?? - [], - ); - } -} - -class WorkItem { - final String? id; - final ActivityMaster? activityMaster; - final WorkArea? workArea; - final double? plannedWork; - final double? completedWork; - final List preSignedUrls; - - WorkItem({ - this.id, - this.activityMaster, - this.workArea, - this.plannedWork, - this.completedWork, - this.preSignedUrls = const [], - }); - - factory WorkItem.fromJson(Map 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?) - ?.map((e) => e.toString()) - .toList() ?? - [], - ); - } -} - -class ActivityMaster { - final String? id; // ✅ Added - final String activityName; - - ActivityMaster({ - this.id, - required this.activityName, - }); - - factory ActivityMaster.fromJson(Map 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 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 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 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 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 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 preSignedUrls; - - Comment({ - required this.comment, - required this.commentedBy, - required this.timestamp, - required this.preSignedUrls, - }); - - factory Comment.fromJson(Map 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?) - ?.map((e) => e.toString()) - .toList() ?? - [], - ); - } -} diff --git a/lib/model/dailyTaskPlanning/daily_task_planning_filter.dart b/lib/model/dailyTaskPlanning/daily_task_planning_filter.dart deleted file mode 100644 index aa9a521..0000000 --- a/lib/model/dailyTaskPlanning/daily_task_planning_filter.dart +++ /dev/null @@ -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 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, - }); - }, - ), - ), - ), - ], - ), - ), - ); - }); - } -} diff --git a/lib/model/dailyTaskPlanning/daily_task_planning_model.dart b/lib/model/dailyTaskPlanning/daily_task_planning_model.dart deleted file mode 100644 index 7609903..0000000 --- a/lib/model/dailyTaskPlanning/daily_task_planning_model.dart +++ /dev/null @@ -1,246 +0,0 @@ -class TaskPlanningDetailsModel { - final List 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 json) { - return TaskPlanningDetailsModel( - buildings: (json['buildings'] as List?) - ?.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 floors; - - Building({ - required this.id, - required this.name, - required this.description, - required this.floors, - }); - - factory Building.fromJson(Map 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 workAreas; - - Floor({ - required this.id, - required this.floorName, - required this.workAreas, - }); - - factory Floor.fromJson(Map 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 workItems; - - WorkArea({ - required this.id, - required this.areaName, - required this.workItems, - }); - - factory WorkArea.fromJson(Map 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 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 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) - : null, - activityMaster: json['activityMaster'] != null - ? ActivityMaster.fromJson( - json['activityMaster'] as Map) - : null, - workCategoryMaster: json['workCategoryMaster'] != null - ? WorkCategoryMaster.fromJson( - json['workCategoryMaster'] as Map) - : 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) - : null, - ); - } -} - -class WorkAreaBasic { - final String? id; - final String? name; - - WorkAreaBasic({this.id, this.name}); - - factory WorkAreaBasic.fromJson(Map 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 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 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 json) { - return Tenant( - id: json['id'] as String?, - name: json['name'] as String?, - ); - } -} diff --git a/lib/model/dailyTaskPlanning/master_work_category_model.dart b/lib/model/dailyTaskPlanning/master_work_category_model.dart deleted file mode 100644 index bf87dd0..0000000 --- a/lib/model/dailyTaskPlanning/master_work_category_model.dart +++ /dev/null @@ -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 json) { - return WorkCategoryModel( - id: json['id'] ?? '', - name: json['name'] ?? '', - description: json['description'] ?? '', - isSystem: json['isSystem'] ?? false, - ); - } - - Map toJson() { - return { - 'id': id, - 'name': name, - 'description': description, - 'isSystem': isSystem, - }; - } -} diff --git a/lib/model/dailyTaskPlanning/report_action_bottom_sheet.dart b/lib/model/dailyTaskPlanning/report_action_bottom_sheet.dart deleted file mode 100644 index 54b333b..0000000 --- a/lib/model/dailyTaskPlanning/report_action_bottom_sheet.dart +++ /dev/null @@ -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 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 createState() => - _ReportActionBottomSheetState(); -} - -class _Member { - final String firstName; - _Member(this.firstName); -} - -class _ReportActionBottomSheetState extends State - 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).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( - 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?) - ?.isNotEmpty == - true) - buildReportedImagesSection( - imageUrls: List.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( - 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( - value: status.name, - child: Row( - children: [ - Radio( - 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((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?)?.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>.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), - ), - ), - ), - ], - ), - ), - ), - ], - ), - ); - } -} diff --git a/lib/model/dailyTaskPlanning/report_action_widgets.dart b/lib/model/dailyTaskPlanning/report_action_widgets.dart deleted file mode 100644 index 3192e90..0000000 --- a/lib/model/dailyTaskPlanning/report_action_widgets.dart +++ /dev/null @@ -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 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 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> 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.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 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(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 ''; - } -} diff --git a/lib/model/dailyTaskPlanning/report_task_bottom_sheet.dart b/lib/model/dailyTaskPlanning/report_task_bottom_sheet.dart deleted file mode 100644 index bd05421..0000000 --- a/lib/model/dailyTaskPlanning/report_task_bottom_sheet.dart +++ /dev/null @@ -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 taskData; - final VoidCallback? onReportSuccess; - - const ReportTaskBottomSheet({ - super.key, - required this.taskData, - this.onReportSuccess, - }); - - @override - State createState() => _ReportTaskBottomSheetState(); -} - -class _ReportTaskBottomSheetState extends State - 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 _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), - ], - ), - ), - ), - ], - ), - ], - ); - } -} \ No newline at end of file diff --git a/lib/model/dailyTaskPlanning/task_action_buttons.dart b/lib/model/dailyTaskPlanning/task_action_buttons.dart deleted file mode 100644 index e15d627..0000000 --- a/lib/model/dailyTaskPlanning/task_action_buttons.dart +++ /dev/null @@ -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 _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, - }; - } -} diff --git a/lib/model/dailyTaskPlanning/task_list_model.dart b/lib/model/dailyTaskPlanning/task_list_model.dart deleted file mode 100644 index 98e32ee..0000000 --- a/lib/model/dailyTaskPlanning/task_list_model.dart +++ /dev/null @@ -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 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 listFromJSON(List list) { - return list.map((e) => TaskListModel.fromJSON(e)).toList(); - } - - static List? _dummyList; - - static Future> get dummyList async { - if (_dummyList == null) { - dynamic data = json.decode(await getData()); - _dummyList = listFromJSON(data); - } - - return _dummyList!; - } - - static Future getData() async { - return await rootBundle.loadString('assets/data/task_list.json'); - } -} diff --git a/lib/model/dailyTaskPlanning/work_status_model.dart b/lib/model/dailyTaskPlanning/work_status_model.dart deleted file mode 100644 index 91feab7..0000000 --- a/lib/model/dailyTaskPlanning/work_status_model.dart +++ /dev/null @@ -1,53 +0,0 @@ -class WorkStatusResponseModel { - final bool success; - final String message; - final List 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 json) { - return WorkStatusResponseModel( - success: json['success'], - message: json['message'], - data: List.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 json) { - return WorkStatus( - id: json['id'], - name: json['name'], - description: json['description'], - isSystem: json['isSystem'], - ); - } -} diff --git a/lib/model/tenant/tenant_list_model.dart b/lib/model/tenant/tenant_list_model.dart deleted file mode 100644 index 34de63b..0000000 --- a/lib/model/tenant/tenant_list_model.dart +++ /dev/null @@ -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 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 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 json) { - return Industry( - id: json['id'] ?? '', - name: json['name'] ?? '', - ); - } - - Map 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 json) { - return TenantStatus( - id: json['id'] ?? '', - name: json['name'] ?? '', - ); - } - - Map toJson() { - return { - 'id': id, - 'name': name, - }; - } -} diff --git a/lib/model/tenant/tenant_services_model.dart b/lib/model/tenant/tenant_services_model.dart deleted file mode 100644 index 2416e38..0000000 --- a/lib/model/tenant/tenant_services_model.dart +++ /dev/null @@ -1,78 +0,0 @@ -class ServiceListResponse { - final bool success; - final String message; - final List 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 json) { - return ServiceListResponse( - success: json['success'] ?? false, - message: json['message'] ?? '', - data: (json['data'] as List?) - ?.map((e) => Service.fromJson(e)) - .toList() ?? - [], - errors: json['errors'], - statusCode: json['statusCode'] ?? 0, - timestamp: json['timestamp'] ?? '', - ); - } - - Map 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 json) { - return Service( - id: json['id'] ?? '', - name: json['name'] ?? '', - description: json['description'] ?? '', - isSystem: json['isSystem'] ?? false, - isActive: json['isActive'] ?? false, - ); - } - - Map toJson() { - return { - 'id': id, - 'name': name, - 'description': description, - 'isSystem': isSystem, - 'isActive': isActive, - }; - } -} diff --git a/lib/routes.dart b/lib/routes.dart index d272b3d..b289adc 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -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(), diff --git a/lib/view/taskPlanning/daily_progress.dart b/lib/view/taskPlanning/daily_progress.dart deleted file mode 100644 index c102803..0000000 --- a/lib/view/taskPlanning/daily_progress.dart +++ /dev/null @@ -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 createState() => - _DailyProgressReportScreenState(); -} - -class TaskChartData { - final String label; - final num value; - final Color color; - - TaskChartData(this.label, this.value, this.color); -} - -class _DailyProgressReportScreenState extends State - with UIMixin { - final DailyTaskController dailyTaskController = - Get.put(DailyTaskController()); - final PermissionController permissionController = - Get.find(); - final ProjectController projectController = Get.find(); - 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(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( - 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( - 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 _openFilterSheet() async { - final result = await showModalBottomSheet>( - 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 _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 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(), - ); - }) - ], - ); - }, - ), - ); - }); - } -} diff --git a/lib/view/taskPlanning/daily_task_planning.dart b/lib/view/taskPlanning/daily_task_planning.dart deleted file mode 100644 index ebf4b3d..0000000 --- a/lib/view/taskPlanning/daily_task_planning.dart +++ /dev/null @@ -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 createState() => - _DailyTaskPlanningScreenState(); -} - -class _DailyTaskPlanningScreenState extends State - with UIMixin { - final DailyTaskPlanningController dailyTaskPlanningController = - Get.put(DailyTaskPlanningController()); - final PermissionController permissionController = - Get.put(PermissionController()); - final ProjectController projectController = Get.find(); - 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( - 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( - 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( - 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 = {}; - final floorExpansionState = {}; - - 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( - 0, (sum, wi) => sum + (wi.workItem.plannedWork ?? 0)); - final totalCompleted = workItems.fold(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(), - ); - }); - }); - } -} diff --git a/lib/view/tenant/tenant_selection_screen.dart b/lib/view/tenant/tenant_selection_screen.dart deleted file mode 100644 index d63a8b8..0000000 --- a/lib/view/tenant/tenant_selection_screen.dart +++ /dev/null @@ -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 createState() => _TenantSelectionScreenState(); -} - -class _TenantSelectionScreenState extends State - with UIMixin, SingleTickerProviderStateMixin { - late final TenantSelectionController _controller; - late final AnimationController _logoAnimController; - late final Animation _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(); - super.dispose(); - } - - Future _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 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; -}