- Updated import paths across multiple files to reflect the new package name. - Changed application name and identifiers in CMakeLists.txt, Runner.rc, and other configuration files. - Modified web index.html and manifest.json to update the app title and name. - Adjusted macOS and Windows project settings to align with the new application name. - Ensured consistency in naming across all relevant files and directories.
368 lines
12 KiB
Dart
368 lines
12 KiB
Dart
import 'package:get/get.dart';
|
|
import 'package:on_field_work/helpers/services/app_logger.dart';
|
|
import 'package:on_field_work/helpers/services/api_service.dart';
|
|
import 'package:on_field_work/helpers/widgets/my_form_validator.dart';
|
|
import 'package:on_field_work/helpers/widgets/my_snackbar.dart';
|
|
import 'package:on_field_work/model/project_model.dart';
|
|
import 'package:on_field_work/model/dailyTaskPlanning/daily_task_planning_model.dart';
|
|
import 'package:on_field_work/model/employees/employee_model.dart';
|
|
|
|
class DailyTaskPlanningController extends GetxController {
|
|
List<ProjectModel> projects = [];
|
|
RxList<EmployeeModel> employees = <EmployeeModel>[].obs;
|
|
RxList<EmployeeModel> selectedEmployees = <EmployeeModel>[].obs;
|
|
List<EmployeeModel> allEmployeesCache = [];
|
|
List<TaskPlanningDetailsModel> dailyTasks = [];
|
|
RxMap<String, RxBool> uploadingStates = <String, RxBool>{}.obs;
|
|
|
|
MyFormValidator basicValidator = MyFormValidator();
|
|
List<Map<String, dynamic>> roles = [];
|
|
RxBool isAssigningTask = false.obs;
|
|
RxnString selectedRoleId = RxnString();
|
|
RxBool isFetchingTasks = true.obs;
|
|
RxBool isFetchingProjects = true.obs;
|
|
RxBool isFetchingEmployees = true.obs;
|
|
|
|
/// New: track per-building loading and loaded state for lazy infra loading
|
|
RxMap<String, RxBool> buildingLoadingStates = <String, RxBool>{}.obs;
|
|
final Set<String> buildingsWithDetails = <String>{};
|
|
|
|
@override
|
|
void onInit() {
|
|
super.onInit();
|
|
fetchRoles();
|
|
}
|
|
|
|
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() {
|
|
selectedEmployees.value =
|
|
employees.where((e) => uploadingStates[e.id]?.value == true).toList();
|
|
logSafe("Updated selected employees", level: LogLevel.debug);
|
|
}
|
|
|
|
void onRoleSelected(String? roleId) {
|
|
selectedRoleId.value = roleId;
|
|
logSafe("Role selected", level: LogLevel.info);
|
|
}
|
|
|
|
Future<void> fetchRoles() async {
|
|
logSafe("Fetching roles...", level: LogLevel.info);
|
|
final result = await ApiService.getRoles();
|
|
if (result != null) {
|
|
roles = List<Map<String, dynamic>>.from(result);
|
|
logSafe("Roles fetched successfully", level: LogLevel.info);
|
|
update();
|
|
} else {
|
|
logSafe("Failed to fetch roles", level: LogLevel.error);
|
|
}
|
|
}
|
|
|
|
Future<bool> assignDailyTask({
|
|
required String workItemId,
|
|
required int plannedTask,
|
|
required String description,
|
|
required List<String> taskTeam,
|
|
DateTime? assignmentDate,
|
|
String? organizationId,
|
|
String? serviceId,
|
|
}) 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,
|
|
organizationId: organizationId,
|
|
serviceId: serviceId,
|
|
);
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// Fetch buildings list only (no deep area/workItem calls) for initial load.
|
|
Future<void> fetchTaskData(String? projectId, {String? serviceId}) async {
|
|
if (projectId == null) return;
|
|
|
|
isFetchingTasks.value = true;
|
|
try {
|
|
final infraResponse = await ApiService.getInfraDetails(
|
|
projectId,
|
|
serviceId: serviceId,
|
|
);
|
|
final infraData = infraResponse?['data'] as List<dynamic>?;
|
|
|
|
if (infraData == null || infraData.isEmpty) {
|
|
dailyTasks = [];
|
|
return;
|
|
}
|
|
|
|
// Filter buildings with 0 planned & completed work
|
|
final filteredBuildings = infraData.where((b) {
|
|
final planned = (b['plannedWork'] as num?)?.toDouble() ?? 0;
|
|
final completed = (b['completedWork'] as num?)?.toDouble() ?? 0;
|
|
return planned > 0 || completed > 0;
|
|
}).toList();
|
|
|
|
dailyTasks = filteredBuildings.map((buildingJson) {
|
|
final building = Building(
|
|
id: buildingJson['id'],
|
|
name: buildingJson['buildingName'],
|
|
description: buildingJson['description'],
|
|
floors: [],
|
|
plannedWork: (buildingJson['plannedWork'] as num?)?.toDouble() ?? 0,
|
|
completedWork:
|
|
(buildingJson['completedWork'] as num?)?.toDouble() ?? 0,
|
|
);
|
|
|
|
return TaskPlanningDetailsModel(
|
|
id: building.id,
|
|
name: building.name,
|
|
projectAddress: "",
|
|
contactPerson: "",
|
|
startDate: DateTime.now(),
|
|
endDate: DateTime.now(),
|
|
projectStatusId: "",
|
|
buildings: [building],
|
|
);
|
|
}).toList();
|
|
|
|
buildingLoadingStates.clear();
|
|
buildingsWithDetails.clear();
|
|
} catch (e, stack) {
|
|
logSafe("Error fetching daily task data",
|
|
level: LogLevel.error, error: e, stackTrace: stack);
|
|
} finally {
|
|
isFetchingTasks.value = false;
|
|
update();
|
|
}
|
|
}
|
|
|
|
/// Fetch full infra for a single building (floors, workAreas, workItems).
|
|
/// Called lazily when user expands a building in the UI.
|
|
Future<void> fetchBuildingInfra(
|
|
String buildingId, String projectId, String? serviceId) async {
|
|
if (buildingId.isEmpty) return;
|
|
|
|
// mark loading
|
|
buildingLoadingStates.putIfAbsent(buildingId, () => true.obs);
|
|
buildingLoadingStates[buildingId]!.value = true;
|
|
update();
|
|
|
|
try {
|
|
// Re-use getInfraDetails and find the building entry for the requested buildingId
|
|
final infraResponse =
|
|
await ApiService.getInfraDetails(projectId, serviceId: serviceId);
|
|
final infraData = infraResponse?['data'] as List<dynamic>? ?? [];
|
|
|
|
final buildingJson = infraData.firstWhere(
|
|
(b) => b['id'].toString() == buildingId.toString(),
|
|
orElse: () => null,
|
|
);
|
|
|
|
if (buildingJson == null) {
|
|
logSafe("Building $buildingId not found in infra response",
|
|
level: LogLevel.warning);
|
|
return;
|
|
}
|
|
|
|
// Build floors & workAreas for this building
|
|
final building = Building(
|
|
id: buildingJson['id'],
|
|
name: buildingJson['buildingName'],
|
|
description: buildingJson['description'],
|
|
floors:
|
|
(buildingJson['floors'] as List<dynamic>? ?? []).map((floorJson) {
|
|
return Floor(
|
|
id: floorJson['id'],
|
|
floorName: floorJson['floorName'],
|
|
workAreas: (floorJson['workAreas'] as List<dynamic>? ?? [])
|
|
.map((areaJson) {
|
|
return WorkArea(
|
|
id: areaJson['id'],
|
|
areaName: areaJson['areaName'],
|
|
workItems: [], // will populate later
|
|
);
|
|
}).toList(),
|
|
);
|
|
}).toList(),
|
|
plannedWork: (buildingJson['plannedWork'] as num?)?.toDouble() ?? 0,
|
|
completedWork: (buildingJson['completedWork'] as num?)?.toDouble() ?? 0,
|
|
);
|
|
|
|
// For each workArea, fetch its work items and populate
|
|
await Future.wait(
|
|
building.floors.expand((f) => f.workAreas).map((area) async {
|
|
try {
|
|
final taskResponse = await ApiService.getWorkItemsByWorkArea(area.id,
|
|
serviceId: serviceId);
|
|
final taskData = taskResponse?['data'] as List<dynamic>? ?? [];
|
|
area.workItems.addAll(taskData.map((taskJson) => 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);
|
|
}
|
|
}));
|
|
|
|
// Merge/replace the building into dailyTasks
|
|
bool merged = false;
|
|
for (var t in dailyTasks) {
|
|
final idx = t.buildings
|
|
.indexWhere((b) => b.id.toString() == building.id.toString());
|
|
if (idx != -1) {
|
|
t.buildings[idx] = building;
|
|
merged = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!merged) {
|
|
// If not present, add a new TaskPlanningDetailsModel wrapper (fallback)
|
|
dailyTasks.add(TaskPlanningDetailsModel(
|
|
id: building.id,
|
|
name: building.name,
|
|
projectAddress: "",
|
|
contactPerson: "",
|
|
startDate: DateTime.now(),
|
|
endDate: DateTime.now(),
|
|
projectStatusId: "",
|
|
buildings: [building],
|
|
));
|
|
}
|
|
|
|
// Mark as loaded
|
|
buildingsWithDetails.add(buildingId.toString());
|
|
} catch (e, stack) {
|
|
logSafe("Error fetching infra for building $buildingId",
|
|
level: LogLevel.error, error: e, stackTrace: stack);
|
|
} finally {
|
|
buildingLoadingStates.putIfAbsent(buildingId, () => false.obs);
|
|
buildingLoadingStates[buildingId]!.value = false;
|
|
update();
|
|
}
|
|
}
|
|
|
|
Future<void> fetchEmployeesByProjectService({
|
|
required String projectId,
|
|
String? serviceId,
|
|
String? organizationId,
|
|
}) async {
|
|
isFetchingEmployees.value = true;
|
|
|
|
try {
|
|
final response = await ApiService.getEmployeesByProjectService(
|
|
projectId,
|
|
serviceId: serviceId ?? '',
|
|
organizationId: organizationId ?? '',
|
|
);
|
|
|
|
if (response != null && response.isNotEmpty) {
|
|
employees
|
|
.assignAll(response.map((json) => EmployeeModel.fromJson(json)));
|
|
|
|
if (serviceId == null && organizationId == null) {
|
|
allEmployeesCache = List.from(employees);
|
|
}
|
|
|
|
final currentEmployeeIds = employees.map((e) => e.id).toSet();
|
|
|
|
uploadingStates
|
|
.removeWhere((key, _) => !currentEmployeeIds.contains(key));
|
|
employees.forEach((emp) {
|
|
uploadingStates.putIfAbsent(emp.id, () => false.obs);
|
|
});
|
|
|
|
selectedEmployees
|
|
.removeWhere((e) => !currentEmployeeIds.contains(e.id));
|
|
|
|
logSafe("Employees fetched: ${employees.length}", level: LogLevel.info);
|
|
} else {
|
|
employees.clear();
|
|
uploadingStates.clear();
|
|
selectedEmployees.clear();
|
|
logSafe(
|
|
serviceId != null || organizationId != null
|
|
? "Filtered employees empty"
|
|
: "No employees found",
|
|
level: LogLevel.warning,
|
|
);
|
|
}
|
|
} catch (e, stack) {
|
|
logSafe("Error fetching employees",
|
|
level: LogLevel.error, error: e, stackTrace: stack);
|
|
|
|
if (serviceId == null &&
|
|
organizationId == null &&
|
|
allEmployeesCache.isNotEmpty) {
|
|
employees.assignAll(allEmployeesCache);
|
|
|
|
final cachedEmployeeIds = employees.map((e) => e.id).toSet();
|
|
uploadingStates
|
|
.removeWhere((key, _) => !cachedEmployeeIds.contains(key));
|
|
employees.forEach((emp) {
|
|
uploadingStates.putIfAbsent(emp.id, () => false.obs);
|
|
});
|
|
|
|
selectedEmployees.removeWhere((e) => !cachedEmployeeIds.contains(e.id));
|
|
} else {
|
|
employees.clear();
|
|
uploadingStates.clear();
|
|
selectedEmployees.clear();
|
|
}
|
|
} finally {
|
|
isFetchingEmployees.value = false;
|
|
update();
|
|
}
|
|
}
|
|
}
|