removed unwanted files
This commit is contained in:
parent
3515cab0d5
commit
4feb2875f0
@ -1,152 +0,0 @@
|
|||||||
import 'package:get/get.dart';
|
|
||||||
import 'package:marco/helpers/services/app_logger.dart';
|
|
||||||
import 'package:marco/helpers/services/api_service.dart';
|
|
||||||
import 'package:marco/helpers/widgets/my_form_validator.dart';
|
|
||||||
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
|
||||||
import 'package:marco/model/dailyTaskPlanning/master_work_category_model.dart';
|
|
||||||
|
|
||||||
class AddTaskController extends GetxController {
|
|
||||||
RxMap<String, RxBool> uploadingStates = <String, RxBool>{}.obs;
|
|
||||||
MyFormValidator basicValidator = MyFormValidator();
|
|
||||||
RxnString selectedCategoryId = RxnString();
|
|
||||||
RxnString selectedCategoryName = RxnString();
|
|
||||||
var categoryIdNameMap = <String, String>{}.obs;
|
|
||||||
|
|
||||||
List<Map<String, dynamic>> roles = [];
|
|
||||||
RxnString selectedRoleId = RxnString();
|
|
||||||
RxBool isLoadingWorkMasterCategories = false.obs;
|
|
||||||
RxList<WorkCategoryModel> workMasterCategories = <WorkCategoryModel>[].obs;
|
|
||||||
|
|
||||||
RxBool isLoading = false.obs;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void onInit() {
|
|
||||||
super.onInit();
|
|
||||||
fetchWorkMasterCategories();
|
|
||||||
}
|
|
||||||
|
|
||||||
String? formFieldValidator(String? value, {required String fieldType}) {
|
|
||||||
if (value == null || value.trim().isEmpty) {
|
|
||||||
return 'This field is required';
|
|
||||||
}
|
|
||||||
if (fieldType == "target" && int.tryParse(value.trim()) == null) {
|
|
||||||
return 'Please enter a valid number';
|
|
||||||
}
|
|
||||||
if (fieldType == "description" && value.trim().length < 5) {
|
|
||||||
return 'Description must be at least 5 characters';
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> assignDailyTask({
|
|
||||||
required String workItemId,
|
|
||||||
required int plannedTask,
|
|
||||||
required String description,
|
|
||||||
required List<String> taskTeam,
|
|
||||||
DateTime? assignmentDate,
|
|
||||||
}) async {
|
|
||||||
logSafe("Starting task assignment...", level: LogLevel.info);
|
|
||||||
|
|
||||||
final response = await ApiService.assignDailyTask(
|
|
||||||
workItemId: workItemId,
|
|
||||||
plannedTask: plannedTask,
|
|
||||||
description: description,
|
|
||||||
taskTeam: taskTeam,
|
|
||||||
assignmentDate: assignmentDate,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (response == true) {
|
|
||||||
logSafe("Task assigned successfully.", level: LogLevel.info);
|
|
||||||
showAppSnackbar(
|
|
||||||
title: "Success",
|
|
||||||
message: "Task assigned successfully!",
|
|
||||||
type: SnackbarType.success,
|
|
||||||
);
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
logSafe("Failed to assign task.", level: LogLevel.error);
|
|
||||||
showAppSnackbar(
|
|
||||||
title: "Error",
|
|
||||||
message: "Failed to assign task.",
|
|
||||||
type: SnackbarType.error,
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> createTask({
|
|
||||||
required String parentTaskId,
|
|
||||||
required String workAreaId,
|
|
||||||
required String activityId,
|
|
||||||
required int plannedTask,
|
|
||||||
required String comment,
|
|
||||||
required String categoryId,
|
|
||||||
DateTime? assignmentDate,
|
|
||||||
}) async {
|
|
||||||
logSafe("Creating new task...", level: LogLevel.info);
|
|
||||||
|
|
||||||
final response = await ApiService.createTask(
|
|
||||||
parentTaskId: parentTaskId,
|
|
||||||
plannedTask: plannedTask,
|
|
||||||
comment: comment,
|
|
||||||
workAreaId: workAreaId,
|
|
||||||
activityId: activityId,
|
|
||||||
assignmentDate: assignmentDate,
|
|
||||||
categoryId: categoryId,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (response == true) {
|
|
||||||
logSafe("Task created successfully.", level: LogLevel.info);
|
|
||||||
showAppSnackbar(
|
|
||||||
title: "Success",
|
|
||||||
message: "Task created successfully!",
|
|
||||||
type: SnackbarType.success,
|
|
||||||
);
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
logSafe("Failed to create task.", level: LogLevel.error);
|
|
||||||
showAppSnackbar(
|
|
||||||
title: "Error",
|
|
||||||
message: "Failed to create task.",
|
|
||||||
type: SnackbarType.error,
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> fetchWorkMasterCategories() async {
|
|
||||||
isLoadingWorkMasterCategories.value = true;
|
|
||||||
|
|
||||||
try {
|
|
||||||
final response = await ApiService.getMasterWorkCategories();
|
|
||||||
if (response != null) {
|
|
||||||
final dataList = response['data'] ?? [];
|
|
||||||
|
|
||||||
final parsedList = List<WorkCategoryModel>.from(
|
|
||||||
dataList.map((e) => WorkCategoryModel.fromJson(e)),
|
|
||||||
);
|
|
||||||
|
|
||||||
workMasterCategories.assignAll(parsedList);
|
|
||||||
final mapped = {for (var item in parsedList) item.id: item.name};
|
|
||||||
categoryIdNameMap.assignAll(mapped);
|
|
||||||
|
|
||||||
logSafe("Work categories fetched: ${dataList.length}", level: LogLevel.info);
|
|
||||||
} else {
|
|
||||||
logSafe("No work categories found or API call failed.", level: LogLevel.warning);
|
|
||||||
}
|
|
||||||
} catch (e, st) {
|
|
||||||
logSafe("Error parsing work categories", level: LogLevel.error, error: e, stackTrace: st);
|
|
||||||
workMasterCategories.clear();
|
|
||||||
categoryIdNameMap.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
isLoadingWorkMasterCategories.value = false;
|
|
||||||
update();
|
|
||||||
}
|
|
||||||
|
|
||||||
void selectCategory(String id) {
|
|
||||||
selectedCategoryId.value = id;
|
|
||||||
selectedCategoryName.value = categoryIdNameMap[id];
|
|
||||||
logSafe("Category selected", level: LogLevel.debug, );
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,146 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:get/get.dart';
|
|
||||||
import 'package:marco/helpers/services/app_logger.dart';
|
|
||||||
import 'package:marco/helpers/services/api_service.dart';
|
|
||||||
import 'package:marco/model/project_model.dart';
|
|
||||||
import 'package:marco/model/dailyTaskPlanning/daily_task_model.dart';
|
|
||||||
|
|
||||||
class DailyTaskController extends GetxController {
|
|
||||||
List<ProjectModel> projects = [];
|
|
||||||
String? selectedProjectId;
|
|
||||||
|
|
||||||
DateTime? startDateTask;
|
|
||||||
DateTime? endDateTask;
|
|
||||||
|
|
||||||
List<TaskModel> dailyTasks = [];
|
|
||||||
final RxSet<String> expandedDates = <String>{}.obs;
|
|
||||||
|
|
||||||
void toggleDate(String dateKey) {
|
|
||||||
if (expandedDates.contains(dateKey)) {
|
|
||||||
expandedDates.remove(dateKey);
|
|
||||||
} else {
|
|
||||||
expandedDates.add(dateKey);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
RxBool isLoading = true.obs;
|
|
||||||
RxBool isLoadingMore = false.obs;
|
|
||||||
Map<String, List<TaskModel>> groupedDailyTasks = {};
|
|
||||||
// Pagination
|
|
||||||
int currentPage = 1;
|
|
||||||
int pageSize = 20;
|
|
||||||
bool hasMore = true;
|
|
||||||
@override
|
|
||||||
void onInit() {
|
|
||||||
super.onInit();
|
|
||||||
_initializeDefaults();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _initializeDefaults() {
|
|
||||||
_setDefaultDateRange();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _setDefaultDateRange() {
|
|
||||||
final today = DateTime.now();
|
|
||||||
startDateTask = today.subtract(const Duration(days: 7));
|
|
||||||
endDateTask = today;
|
|
||||||
|
|
||||||
logSafe(
|
|
||||||
"Default date range set: $startDateTask to $endDateTask",
|
|
||||||
level: LogLevel.info,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> fetchTaskData(
|
|
||||||
String projectId, {
|
|
||||||
List<String>? serviceIds,
|
|
||||||
int pageNumber = 1,
|
|
||||||
int pageSize = 20,
|
|
||||||
bool isLoadMore = false,
|
|
||||||
}) async {
|
|
||||||
if (!isLoadMore) {
|
|
||||||
isLoading.value = true;
|
|
||||||
currentPage = 1;
|
|
||||||
hasMore = true;
|
|
||||||
groupedDailyTasks.clear();
|
|
||||||
dailyTasks.clear();
|
|
||||||
} else {
|
|
||||||
isLoadingMore.value = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
final response = await ApiService.getDailyTasks(
|
|
||||||
projectId,
|
|
||||||
dateFrom: startDateTask,
|
|
||||||
dateTo: endDateTask,
|
|
||||||
serviceIds: serviceIds,
|
|
||||||
pageNumber: pageNumber,
|
|
||||||
pageSize: pageSize,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (response != null && response.isNotEmpty) {
|
|
||||||
for (var taskJson in response) {
|
|
||||||
final task = TaskModel.fromJson(taskJson);
|
|
||||||
final assignmentDateKey =
|
|
||||||
task.assignmentDate.toIso8601String().split('T')[0];
|
|
||||||
groupedDailyTasks.putIfAbsent(assignmentDateKey, () => []).add(task);
|
|
||||||
}
|
|
||||||
dailyTasks = groupedDailyTasks.values.expand((list) => list).toList();
|
|
||||||
currentPage = pageNumber;
|
|
||||||
} else {
|
|
||||||
hasMore = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
isLoading.value = false;
|
|
||||||
isLoadingMore.value = false;
|
|
||||||
|
|
||||||
update();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> selectDateRangeForTaskData(
|
|
||||||
BuildContext context,
|
|
||||||
DailyTaskController controller,
|
|
||||||
) async {
|
|
||||||
final picked = await showDateRangePicker(
|
|
||||||
context: context,
|
|
||||||
firstDate: DateTime(2022),
|
|
||||||
lastDate: DateTime.now(),
|
|
||||||
initialDateRange: DateTimeRange(
|
|
||||||
start:
|
|
||||||
startDateTask ?? DateTime.now().subtract(const Duration(days: 7)),
|
|
||||||
end: endDateTask ?? DateTime.now(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (picked == null) {
|
|
||||||
logSafe("Date range picker cancelled by user.", level: LogLevel.debug);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
startDateTask = picked.start;
|
|
||||||
endDateTask = picked.end;
|
|
||||||
|
|
||||||
logSafe(
|
|
||||||
"Date range selected: $startDateTask to $endDateTask",
|
|
||||||
level: LogLevel.info,
|
|
||||||
);
|
|
||||||
|
|
||||||
// ✅ Add null check before calling fetchTaskData
|
|
||||||
final projectId = controller.selectedProjectId;
|
|
||||||
if (projectId != null && projectId.isNotEmpty) {
|
|
||||||
await controller.fetchTaskData(projectId);
|
|
||||||
} else {
|
|
||||||
logSafe("Project ID is null or empty, skipping fetchTaskData",
|
|
||||||
level: LogLevel.warning);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void refreshTasksFromNotification({
|
|
||||||
required String projectId,
|
|
||||||
required String taskAllocationId,
|
|
||||||
}) async {
|
|
||||||
// re-fetch tasks
|
|
||||||
await fetchTaskData(projectId);
|
|
||||||
|
|
||||||
update(); // rebuilds UI
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,279 +0,0 @@
|
|||||||
import 'package:get/get.dart';
|
|
||||||
import 'package:marco/helpers/services/app_logger.dart';
|
|
||||||
import 'package:marco/helpers/services/api_service.dart';
|
|
||||||
import 'package:marco/helpers/widgets/my_form_validator.dart';
|
|
||||||
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
|
||||||
import 'package:marco/model/project_model.dart';
|
|
||||||
import 'package:marco/model/dailyTaskPlanning/daily_task_planning_model.dart';
|
|
||||||
import 'package:marco/model/employees/employee_model.dart';
|
|
||||||
|
|
||||||
class DailyTaskPlanningController extends GetxController {
|
|
||||||
List<ProjectModel> projects = [];
|
|
||||||
List<EmployeeModel> employees = [];
|
|
||||||
List<TaskPlanningDetailsModel> dailyTasks = [];
|
|
||||||
|
|
||||||
RxMap<String, RxBool> uploadingStates = <String, RxBool>{}.obs;
|
|
||||||
RxList<EmployeeModel> selectedEmployees = <EmployeeModel>[].obs;
|
|
||||||
|
|
||||||
MyFormValidator basicValidator = MyFormValidator();
|
|
||||||
List<Map<String, dynamic>> roles = [];
|
|
||||||
RxBool isAssigningTask = false.obs;
|
|
||||||
RxnString selectedRoleId = RxnString();
|
|
||||||
RxBool isLoading = false.obs;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void onInit() {
|
|
||||||
super.onInit();
|
|
||||||
fetchRoles();
|
|
||||||
_initializeDefaults();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _initializeDefaults() {
|
|
||||||
fetchProjects();
|
|
||||||
}
|
|
||||||
|
|
||||||
String? formFieldValidator(String? value, {required String fieldType}) {
|
|
||||||
if (value == null || value.trim().isEmpty) {
|
|
||||||
return 'This field is required';
|
|
||||||
}
|
|
||||||
if (fieldType == "target" && int.tryParse(value.trim()) == null) {
|
|
||||||
return 'Please enter a valid number';
|
|
||||||
}
|
|
||||||
if (fieldType == "description" && value.trim().length < 5) {
|
|
||||||
return 'Description must be at least 5 characters';
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
void updateSelectedEmployees() {
|
|
||||||
final selected =
|
|
||||||
employees.where((e) => uploadingStates[e.id]?.value == true).toList();
|
|
||||||
selectedEmployees.value = selected;
|
|
||||||
logSafe("Updated selected employees", level: LogLevel.debug);
|
|
||||||
}
|
|
||||||
|
|
||||||
void onRoleSelected(String? roleId) {
|
|
||||||
selectedRoleId.value = roleId;
|
|
||||||
logSafe("Role selected", level: LogLevel.info);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> fetchRoles() async {
|
|
||||||
logSafe("Fetching roles...", level: LogLevel.info);
|
|
||||||
final result = await ApiService.getRoles();
|
|
||||||
if (result != null) {
|
|
||||||
roles = List<Map<String, dynamic>>.from(result);
|
|
||||||
logSafe("Roles fetched successfully", level: LogLevel.info);
|
|
||||||
update();
|
|
||||||
} else {
|
|
||||||
logSafe("Failed to fetch roles", level: LogLevel.error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> assignDailyTask({
|
|
||||||
required String workItemId,
|
|
||||||
required int plannedTask,
|
|
||||||
required String description,
|
|
||||||
required List<String> taskTeam,
|
|
||||||
DateTime? assignmentDate,
|
|
||||||
}) async {
|
|
||||||
isAssigningTask.value = true;
|
|
||||||
logSafe("Starting assign task...", level: LogLevel.info);
|
|
||||||
|
|
||||||
final response = await ApiService.assignDailyTask(
|
|
||||||
workItemId: workItemId,
|
|
||||||
plannedTask: plannedTask,
|
|
||||||
description: description,
|
|
||||||
taskTeam: taskTeam,
|
|
||||||
assignmentDate: assignmentDate,
|
|
||||||
);
|
|
||||||
|
|
||||||
isAssigningTask.value = false;
|
|
||||||
|
|
||||||
if (response == true) {
|
|
||||||
logSafe("Task assigned successfully", level: LogLevel.info);
|
|
||||||
showAppSnackbar(
|
|
||||||
title: "Success",
|
|
||||||
message: "Task assigned successfully!",
|
|
||||||
type: SnackbarType.success,
|
|
||||||
);
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
logSafe("Failed to assign task", level: LogLevel.error);
|
|
||||||
showAppSnackbar(
|
|
||||||
title: "Error",
|
|
||||||
message: "Failed to assign task.",
|
|
||||||
type: SnackbarType.error,
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> fetchProjects() async {
|
|
||||||
isLoading.value = true;
|
|
||||||
try {
|
|
||||||
final response = await ApiService.getProjects();
|
|
||||||
if (response?.isEmpty ?? true) {
|
|
||||||
logSafe("No project data found or API call failed",
|
|
||||||
level: LogLevel.warning);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
projects = response!.map((json) => ProjectModel.fromJson(json)).toList();
|
|
||||||
logSafe("Projects fetched: ${projects.length} projects loaded",
|
|
||||||
level: LogLevel.info);
|
|
||||||
update();
|
|
||||||
} catch (e, stack) {
|
|
||||||
logSafe("Error fetching projects",
|
|
||||||
level: LogLevel.error, error: e, stackTrace: stack);
|
|
||||||
} finally {
|
|
||||||
isLoading.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Fetch Infra details and then tasks per work area
|
|
||||||
Future<void> fetchTaskData(String? projectId, {String? serviceId}) async {
|
|
||||||
if (projectId == null) {
|
|
||||||
logSafe("Project ID is null", level: LogLevel.warning);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
isLoading.value = true;
|
|
||||||
try {
|
|
||||||
// Fetch infra details
|
|
||||||
final infraResponse = await ApiService.getInfraDetails(projectId);
|
|
||||||
final infraData = infraResponse?['data'] as List<dynamic>?;
|
|
||||||
|
|
||||||
if (infraData == null || infraData.isEmpty) {
|
|
||||||
logSafe("No infra data found for project $projectId",
|
|
||||||
level: LogLevel.warning);
|
|
||||||
dailyTasks = [];
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Map infra to dailyTasks structure
|
|
||||||
dailyTasks = infraData.map((buildingJson) {
|
|
||||||
final building = Building(
|
|
||||||
id: buildingJson['id'],
|
|
||||||
name: buildingJson['buildingName'],
|
|
||||||
description: buildingJson['description'],
|
|
||||||
floors: (buildingJson['floors'] as List<dynamic>).map((floorJson) {
|
|
||||||
return Floor(
|
|
||||||
id: floorJson['id'],
|
|
||||||
floorName: floorJson['floorName'],
|
|
||||||
workAreas:
|
|
||||||
(floorJson['workAreas'] as List<dynamic>).map((areaJson) {
|
|
||||||
return WorkArea(
|
|
||||||
id: areaJson['id'],
|
|
||||||
areaName: areaJson['areaName'],
|
|
||||||
workItems: [], // Will fill after tasks API
|
|
||||||
);
|
|
||||||
}).toList(),
|
|
||||||
);
|
|
||||||
}).toList(),
|
|
||||||
);
|
|
||||||
|
|
||||||
return TaskPlanningDetailsModel(
|
|
||||||
id: building.id,
|
|
||||||
name: building.name,
|
|
||||||
projectAddress: "",
|
|
||||||
contactPerson: "",
|
|
||||||
startDate: DateTime.now(),
|
|
||||||
endDate: DateTime.now(),
|
|
||||||
projectStatusId: "",
|
|
||||||
buildings: [building],
|
|
||||||
);
|
|
||||||
}).toList();
|
|
||||||
|
|
||||||
// Fetch tasks for each work area, passing serviceId only if selected
|
|
||||||
await Future.wait(dailyTasks
|
|
||||||
.expand((task) => task.buildings)
|
|
||||||
.expand((b) => b.floors)
|
|
||||||
.expand((f) => f.workAreas)
|
|
||||||
.map((area) async {
|
|
||||||
try {
|
|
||||||
final taskResponse = await ApiService.getWorkItemsByWorkArea(
|
|
||||||
area.id,
|
|
||||||
// serviceId: serviceId, // <-- only pass if not null
|
|
||||||
);
|
|
||||||
final taskData = taskResponse?['data'] as List<dynamic>? ?? [];
|
|
||||||
|
|
||||||
area.workItems.addAll(taskData.map((taskJson) {
|
|
||||||
return WorkItemWrapper(
|
|
||||||
workItemId: taskJson['id'],
|
|
||||||
workItem: WorkItem(
|
|
||||||
id: taskJson['id'],
|
|
||||||
activityMaster: taskJson['activityMaster'] != null
|
|
||||||
? ActivityMaster.fromJson(taskJson['activityMaster'])
|
|
||||||
: null,
|
|
||||||
workCategoryMaster: taskJson['workCategoryMaster'] != null
|
|
||||||
? WorkCategoryMaster.fromJson(
|
|
||||||
taskJson['workCategoryMaster'])
|
|
||||||
: null,
|
|
||||||
plannedWork: (taskJson['plannedWork'] as num?)?.toDouble(),
|
|
||||||
completedWork: (taskJson['completedWork'] as num?)?.toDouble(),
|
|
||||||
todaysAssigned:
|
|
||||||
(taskJson['todaysAssigned'] as num?)?.toDouble(),
|
|
||||||
description: taskJson['description'] as String?,
|
|
||||||
taskDate: taskJson['taskDate'] != null
|
|
||||||
? DateTime.tryParse(taskJson['taskDate'])
|
|
||||||
: null,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}));
|
|
||||||
} catch (e, stack) {
|
|
||||||
logSafe("Error fetching tasks for work area ${area.id}",
|
|
||||||
level: LogLevel.error, error: e, stackTrace: stack);
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
logSafe("Fetched infra and tasks for project $projectId",
|
|
||||||
level: LogLevel.info);
|
|
||||||
} catch (e, stack) {
|
|
||||||
logSafe("Error fetching daily task data",
|
|
||||||
level: LogLevel.error, error: e, stackTrace: stack);
|
|
||||||
} finally {
|
|
||||||
isLoading.value = false;
|
|
||||||
update();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> fetchEmployeesByProject(String? projectId) async {
|
|
||||||
if (projectId == null || projectId.isEmpty) {
|
|
||||||
logSafe("Project ID is required but was null or empty",
|
|
||||||
level: LogLevel.error);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
isLoading.value = true;
|
|
||||||
try {
|
|
||||||
final response = await ApiService.getAllEmployeesByProject(projectId);
|
|
||||||
if (response != null && response.isNotEmpty) {
|
|
||||||
employees =
|
|
||||||
response.map((json) => EmployeeModel.fromJson(json)).toList();
|
|
||||||
for (var emp in employees) {
|
|
||||||
uploadingStates[emp.id] = false.obs;
|
|
||||||
}
|
|
||||||
logSafe(
|
|
||||||
"Employees fetched: ${employees.length} for project $projectId",
|
|
||||||
level: LogLevel.info,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
employees = [];
|
|
||||||
logSafe(
|
|
||||||
"No employees found for project $projectId",
|
|
||||||
level: LogLevel.warning,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (e, stack) {
|
|
||||||
logSafe(
|
|
||||||
"Error fetching employees for project $projectId",
|
|
||||||
level: LogLevel.error,
|
|
||||||
error: e,
|
|
||||||
stackTrace: stack,
|
|
||||||
);
|
|
||||||
} finally {
|
|
||||||
isLoading.value = false;
|
|
||||||
update();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,296 +0,0 @@
|
|||||||
import 'dart:convert';
|
|
||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:get/get.dart';
|
|
||||||
import 'package:image_picker/image_picker.dart';
|
|
||||||
import 'package:marco/helpers/services/app_logger.dart';
|
|
||||||
|
|
||||||
import 'package:marco/controller/my_controller.dart';
|
|
||||||
import 'package:marco/controller/task_Planning/daily_task_Planning_controller.dart';
|
|
||||||
import 'package:marco/helpers/services/api_service.dart';
|
|
||||||
import 'package:marco/helpers/widgets/my_form_validator.dart';
|
|
||||||
import 'package:marco/helpers/widgets/my_image_compressor.dart';
|
|
||||||
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
|
||||||
import 'package:marco/model/dailyTaskPlanning/work_status_model.dart';
|
|
||||||
|
|
||||||
enum ApiStatus { idle, loading, success, failure }
|
|
||||||
|
|
||||||
class ReportTaskActionController extends MyController {
|
|
||||||
final RxBool isLoading = false.obs;
|
|
||||||
final Rx<ApiStatus> reportStatus = ApiStatus.idle.obs;
|
|
||||||
final Rx<ApiStatus> commentStatus = ApiStatus.idle.obs;
|
|
||||||
|
|
||||||
final RxList<File> selectedImages = <File>[].obs;
|
|
||||||
final RxList<WorkStatus> workStatus = <WorkStatus>[].obs;
|
|
||||||
final RxList<WorkStatus> workStatuses = <WorkStatus>[].obs;
|
|
||||||
|
|
||||||
final RxBool showAddTaskCheckbox = false.obs;
|
|
||||||
final RxBool isAddTaskChecked = false.obs;
|
|
||||||
|
|
||||||
final RxBool isLoadingWorkStatus = false.obs;
|
|
||||||
final Rxn<Map<String, dynamic>> selectedTask = Rxn<Map<String, dynamic>>();
|
|
||||||
|
|
||||||
final RxString selectedWorkStatusName = ''.obs;
|
|
||||||
|
|
||||||
final MyFormValidator basicValidator = MyFormValidator();
|
|
||||||
final DailyTaskPlanningController taskController = Get.put(DailyTaskPlanningController());
|
|
||||||
final ImagePicker _picker = ImagePicker();
|
|
||||||
|
|
||||||
final assignedDateController = TextEditingController();
|
|
||||||
final workAreaController = TextEditingController();
|
|
||||||
final activityController = TextEditingController();
|
|
||||||
final teamSizeController = TextEditingController();
|
|
||||||
final taskIdController = TextEditingController();
|
|
||||||
final assignedController = TextEditingController();
|
|
||||||
final completedWorkController = TextEditingController();
|
|
||||||
final commentController = TextEditingController();
|
|
||||||
final assignedByController = TextEditingController();
|
|
||||||
final teamMembersController = TextEditingController();
|
|
||||||
final plannedWorkController = TextEditingController();
|
|
||||||
final approvedTaskController = TextEditingController();
|
|
||||||
|
|
||||||
List<TextEditingController> get _allControllers => [
|
|
||||||
assignedDateController,
|
|
||||||
workAreaController,
|
|
||||||
activityController,
|
|
||||||
teamSizeController,
|
|
||||||
taskIdController,
|
|
||||||
assignedController,
|
|
||||||
completedWorkController,
|
|
||||||
commentController,
|
|
||||||
assignedByController,
|
|
||||||
teamMembersController,
|
|
||||||
plannedWorkController,
|
|
||||||
approvedTaskController,
|
|
||||||
];
|
|
||||||
|
|
||||||
@override
|
|
||||||
void onInit() {
|
|
||||||
super.onInit();
|
|
||||||
logSafe("Initializing ReportTaskController...");
|
|
||||||
_initializeFormFields();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void onClose() {
|
|
||||||
for (final controller in _allControllers) {
|
|
||||||
controller.dispose();
|
|
||||||
}
|
|
||||||
logSafe("Disposed all text controllers in ReportTaskActionController.");
|
|
||||||
super.onClose();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _initializeFormFields() {
|
|
||||||
basicValidator
|
|
||||||
..addField('assigned_date', label: "Assigned Date", controller: assignedDateController)
|
|
||||||
..addField('work_area', label: "Work Area", controller: workAreaController)
|
|
||||||
..addField('activity', label: "Activity", controller: activityController)
|
|
||||||
..addField('team_size', label: "Team Size", controller: teamSizeController)
|
|
||||||
..addField('task_id', label: "Task Id", controller: taskIdController)
|
|
||||||
..addField('assigned', label: "Assigned", controller: assignedController)
|
|
||||||
..addField('completed_work', label: "Completed Work", required: true, controller: completedWorkController)
|
|
||||||
..addField('comment', label: "Comment", required: true, controller: commentController)
|
|
||||||
..addField('assigned_by', label: "Assigned By", controller: assignedByController)
|
|
||||||
..addField('team_members', label: "Team Members", controller: teamMembersController)
|
|
||||||
..addField('planned_work', label: "Planned Work", controller: plannedWorkController)
|
|
||||||
..addField('approved_task', label: "Approved Task", required: true, controller: approvedTaskController);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> approveTask({
|
|
||||||
required String projectId,
|
|
||||||
required String comment,
|
|
||||||
required String reportActionId,
|
|
||||||
required String approvedTaskCount,
|
|
||||||
List<File>? images,
|
|
||||||
}) async {
|
|
||||||
logSafe("approveTask() started", sensitive: false);
|
|
||||||
|
|
||||||
if (projectId.isEmpty || reportActionId.isEmpty) {
|
|
||||||
_showError("Project ID and Report Action ID are required.");
|
|
||||||
logSafe("Missing required projectId or reportActionId", level: LogLevel.warning);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
final approvedTaskInt = int.tryParse(approvedTaskCount);
|
|
||||||
final completedWorkInt = int.tryParse(completedWorkController.text.trim());
|
|
||||||
|
|
||||||
if (approvedTaskInt == null) {
|
|
||||||
_showError("Invalid approved task count.");
|
|
||||||
logSafe("Invalid approvedTaskCount: $approvedTaskCount", level: LogLevel.warning);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (completedWorkInt != null && approvedTaskInt > completedWorkInt) {
|
|
||||||
_showError("Approved task count cannot exceed completed work.");
|
|
||||||
logSafe("Validation failed: approved > completed", level: LogLevel.warning);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (comment.trim().isEmpty) {
|
|
||||||
_showError("Comment is required.");
|
|
||||||
logSafe("Comment field is empty", level: LogLevel.warning);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
reportStatus.value = ApiStatus.loading;
|
|
||||||
isLoading.value = true;
|
|
||||||
logSafe("Calling _prepareImages() for approval...");
|
|
||||||
final imageData = await _prepareImages(images);
|
|
||||||
|
|
||||||
logSafe("Calling ApiService.approveTask()");
|
|
||||||
final success = await ApiService.approveTask(
|
|
||||||
id: projectId,
|
|
||||||
workStatus: reportActionId,
|
|
||||||
approvedTask: approvedTaskInt,
|
|
||||||
comment: comment,
|
|
||||||
images: imageData,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (success) {
|
|
||||||
logSafe("Task approved successfully");
|
|
||||||
_showSuccess("Task approved successfully!");
|
|
||||||
await taskController.fetchTaskData(projectId);
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
logSafe("API returned failure on approveTask", level: LogLevel.error);
|
|
||||||
_showError("Failed to approve task.");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
} catch (e, st) {
|
|
||||||
logSafe("Error in approveTask: $e", level: LogLevel.error, error: e, stackTrace: st);
|
|
||||||
_showError("An error occurred.");
|
|
||||||
return false;
|
|
||||||
} finally {
|
|
||||||
isLoading.value = false;
|
|
||||||
Future.delayed(const Duration(milliseconds: 500), () {
|
|
||||||
reportStatus.value = ApiStatus.idle;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> commentTask({
|
|
||||||
required String projectId,
|
|
||||||
required String comment,
|
|
||||||
List<File>? images,
|
|
||||||
}) async {
|
|
||||||
logSafe("commentTask() started", sensitive: false);
|
|
||||||
|
|
||||||
if (commentController.text.trim().isEmpty) {
|
|
||||||
_showError("Comment is required.");
|
|
||||||
logSafe("Comment field is empty", level: LogLevel.warning);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
isLoading.value = true;
|
|
||||||
|
|
||||||
logSafe("Calling _prepareImages() for comment...");
|
|
||||||
final imageData = await _prepareImages(images);
|
|
||||||
|
|
||||||
logSafe("Calling ApiService.commentTask()");
|
|
||||||
final success = await ApiService.commentTask(
|
|
||||||
id: projectId,
|
|
||||||
comment: commentController.text.trim(),
|
|
||||||
images: imageData,
|
|
||||||
).timeout(const Duration(seconds: 30), onTimeout: () {
|
|
||||||
throw Exception("Request timed out.");
|
|
||||||
});
|
|
||||||
|
|
||||||
if (success) {
|
|
||||||
logSafe("Comment added successfully");
|
|
||||||
_showSuccess("Task commented successfully!");
|
|
||||||
await taskController.fetchTaskData(projectId);
|
|
||||||
} else {
|
|
||||||
logSafe("API returned failure on commentTask", level: LogLevel.error);
|
|
||||||
_showError("Failed to comment task.");
|
|
||||||
}
|
|
||||||
} catch (e, st) {
|
|
||||||
logSafe("Error in commentTask: $e", level: LogLevel.error, error: e, stackTrace: st);
|
|
||||||
_showError("An error occurred while commenting the task.");
|
|
||||||
} finally {
|
|
||||||
isLoading.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> fetchWorkStatuses() async {
|
|
||||||
logSafe("Fetching work statuses...");
|
|
||||||
isLoadingWorkStatus.value = true;
|
|
||||||
|
|
||||||
final response = await ApiService.getWorkStatus();
|
|
||||||
if (response != null) {
|
|
||||||
final model = WorkStatusResponseModel.fromJson(response);
|
|
||||||
workStatus.assignAll(model.data);
|
|
||||||
logSafe("Fetched ${model.data.length} work statuses");
|
|
||||||
} else {
|
|
||||||
logSafe("No work statuses found or API call failed", level: LogLevel.warning);
|
|
||||||
}
|
|
||||||
|
|
||||||
isLoadingWorkStatus.value = false;
|
|
||||||
update(['dashboard_controller']);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<List<Map<String, dynamic>>?> _prepareImages(List<File>? images) async {
|
|
||||||
if (images == null || images.isEmpty) {
|
|
||||||
logSafe("_prepareImages: No images selected.");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
logSafe("_prepareImages: Compressing and encoding images...");
|
|
||||||
final results = await Future.wait(images.map((file) async {
|
|
||||||
final compressedBytes = await compressImageToUnder100KB(file);
|
|
||||||
if (compressedBytes == null) return null;
|
|
||||||
|
|
||||||
return {
|
|
||||||
"fileName": file.path.split('/').last,
|
|
||||||
"base64Data": base64Encode(compressedBytes),
|
|
||||||
"contentType": _getContentTypeFromFileName(file.path),
|
|
||||||
"fileSize": compressedBytes.lengthInBytes,
|
|
||||||
"description": "Image uploaded for task",
|
|
||||||
};
|
|
||||||
}));
|
|
||||||
|
|
||||||
logSafe("_prepareImages: Prepared ${results.whereType<Map<String, dynamic>>().length} images.");
|
|
||||||
return results.whereType<Map<String, dynamic>>().toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
String _getContentTypeFromFileName(String fileName) {
|
|
||||||
final ext = fileName.split('.').last.toLowerCase();
|
|
||||||
return switch (ext) {
|
|
||||||
'jpg' || 'jpeg' => 'image/jpeg',
|
|
||||||
'png' => 'image/png',
|
|
||||||
'webp' => 'image/webp',
|
|
||||||
'gif' => 'image/gif',
|
|
||||||
_ => 'application/octet-stream',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> pickImages({required bool fromCamera}) async {
|
|
||||||
logSafe("Opening image picker...");
|
|
||||||
if (fromCamera) {
|
|
||||||
final pickedFile = await _picker.pickImage(source: ImageSource.camera, imageQuality: 75);
|
|
||||||
if (pickedFile != null) {
|
|
||||||
selectedImages.add(File(pickedFile.path));
|
|
||||||
logSafe("Image added from camera: ${pickedFile.path}", );
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
final pickedFiles = await _picker.pickMultiImage(imageQuality: 75);
|
|
||||||
selectedImages.addAll(pickedFiles.map((xfile) => File(xfile.path)));
|
|
||||||
logSafe("${pickedFiles.length} images added from gallery.", );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void removeImageAt(int index) {
|
|
||||||
if (index >= 0 && index < selectedImages.length) {
|
|
||||||
logSafe("Removing image at index $index", );
|
|
||||||
selectedImages.removeAt(index);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _showError(String message) => showAppSnackbar(
|
|
||||||
title: "Error", message: message, type: SnackbarType.error);
|
|
||||||
|
|
||||||
void _showSuccess(String message) => showAppSnackbar(
|
|
||||||
title: "Success", message: message, type: SnackbarType.success);
|
|
||||||
}
|
|
@ -1,248 +0,0 @@
|
|||||||
import 'package:file_picker/file_picker.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:marco/controller/my_controller.dart';
|
|
||||||
import 'package:marco/helpers/widgets/my_form_validator.dart';
|
|
||||||
import 'package:marco/helpers/services/api_service.dart';
|
|
||||||
import 'package:get/get.dart';
|
|
||||||
import 'package:marco/helpers/services/app_logger.dart';
|
|
||||||
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
|
||||||
import 'package:marco/controller/task_Planning/daily_task_Planning_controller.dart';
|
|
||||||
import 'package:image_picker/image_picker.dart';
|
|
||||||
import 'dart:io';
|
|
||||||
import 'dart:convert';
|
|
||||||
import 'package:marco/helpers/widgets/my_image_compressor.dart';
|
|
||||||
|
|
||||||
enum ApiStatus { idle, loading, success, failure }
|
|
||||||
|
|
||||||
final DailyTaskPlanningController taskController = Get.put(DailyTaskPlanningController());
|
|
||||||
final ImagePicker _picker = ImagePicker();
|
|
||||||
|
|
||||||
class ReportTaskController extends MyController {
|
|
||||||
List<PlatformFile> files = [];
|
|
||||||
MyFormValidator basicValidator = MyFormValidator();
|
|
||||||
RxBool isLoading = false.obs;
|
|
||||||
Rx<ApiStatus> reportStatus = ApiStatus.idle.obs;
|
|
||||||
Rx<ApiStatus> commentStatus = ApiStatus.idle.obs;
|
|
||||||
|
|
||||||
RxList<File> selectedImages = <File>[].obs;
|
|
||||||
|
|
||||||
final assignedDateController = TextEditingController();
|
|
||||||
final workAreaController = TextEditingController();
|
|
||||||
final activityController = TextEditingController();
|
|
||||||
final teamSizeController = TextEditingController();
|
|
||||||
final taskIdController = TextEditingController();
|
|
||||||
final assignedController = TextEditingController();
|
|
||||||
final completedWorkController = TextEditingController();
|
|
||||||
final commentController = TextEditingController();
|
|
||||||
final assignedByController = TextEditingController();
|
|
||||||
final teamMembersController = TextEditingController();
|
|
||||||
final plannedWorkController = TextEditingController();
|
|
||||||
|
|
||||||
@override
|
|
||||||
void onInit() {
|
|
||||||
super.onInit();
|
|
||||||
logSafe("Initializing ReportTaskController...");
|
|
||||||
basicValidator
|
|
||||||
..addField('assigned_date', label: "Assigned Date", controller: assignedDateController)
|
|
||||||
..addField('work_area', label: "Work Area", controller: workAreaController)
|
|
||||||
..addField('activity', label: "Activity", controller: activityController)
|
|
||||||
..addField('team_size', label: "Team Size", controller: teamSizeController)
|
|
||||||
..addField('task_id', label: "Task Id", controller: taskIdController)
|
|
||||||
..addField('assigned', label: "Assigned", controller: assignedController)
|
|
||||||
..addField('completed_work', label: "Completed Work", required: true, controller: completedWorkController)
|
|
||||||
..addField('comment', label: "Comment", required: true, controller: commentController)
|
|
||||||
..addField('assigned_by', label: "Assigned By", controller: assignedByController)
|
|
||||||
..addField('team_members', label: "Team Members", controller: teamMembersController)
|
|
||||||
..addField('planned_work', label: "Planned Work", controller: plannedWorkController);
|
|
||||||
logSafe("Form fields initialized.");
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void onClose() {
|
|
||||||
[
|
|
||||||
assignedDateController,
|
|
||||||
workAreaController,
|
|
||||||
activityController,
|
|
||||||
teamSizeController,
|
|
||||||
taskIdController,
|
|
||||||
assignedController,
|
|
||||||
completedWorkController,
|
|
||||||
commentController,
|
|
||||||
assignedByController,
|
|
||||||
teamMembersController,
|
|
||||||
plannedWorkController,
|
|
||||||
].forEach((controller) => controller.dispose());
|
|
||||||
super.onClose();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> reportTask({
|
|
||||||
required String projectId,
|
|
||||||
required String comment,
|
|
||||||
required int completedTask,
|
|
||||||
required List<Map<String, dynamic>> checklist,
|
|
||||||
required DateTime reportedDate,
|
|
||||||
List<File>? images,
|
|
||||||
}) async {
|
|
||||||
logSafe("Reporting task for projectId", );
|
|
||||||
final completedWork = completedWorkController.text.trim();
|
|
||||||
if (completedWork.isEmpty || int.tryParse(completedWork) == null || int.parse(completedWork) < 0) {
|
|
||||||
_showError("Completed work must be a positive number.");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
final commentField = commentController.text.trim();
|
|
||||||
if (commentField.isEmpty) {
|
|
||||||
_showError("Comment is required.");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
reportStatus.value = ApiStatus.loading;
|
|
||||||
isLoading.value = true;
|
|
||||||
|
|
||||||
final imageData = await _prepareImages(images, "task report");
|
|
||||||
|
|
||||||
final success = await ApiService.reportTask(
|
|
||||||
id: projectId,
|
|
||||||
comment: commentField,
|
|
||||||
completedTask: int.parse(completedWork),
|
|
||||||
checkList: checklist,
|
|
||||||
images: imageData,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (success) {
|
|
||||||
reportStatus.value = ApiStatus.success;
|
|
||||||
_showSuccess("Task reported successfully!");
|
|
||||||
await taskController.fetchTaskData(projectId);
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
reportStatus.value = ApiStatus.failure;
|
|
||||||
_showError("Failed to report task.");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
} catch (e, s) {
|
|
||||||
logSafe("Exception while reporting task", level: LogLevel.error, error: e, stackTrace: s);
|
|
||||||
reportStatus.value = ApiStatus.failure;
|
|
||||||
_showError("An error occurred while reporting the task.");
|
|
||||||
return false;
|
|
||||||
} finally {
|
|
||||||
isLoading.value = false;
|
|
||||||
Future.delayed(const Duration(milliseconds: 500), () {
|
|
||||||
reportStatus.value = ApiStatus.idle;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> commentTask({
|
|
||||||
required String projectId,
|
|
||||||
required String comment,
|
|
||||||
List<File>? images,
|
|
||||||
}) async {
|
|
||||||
logSafe("Submitting comment for project", );
|
|
||||||
|
|
||||||
final commentField = commentController.text.trim();
|
|
||||||
if (commentField.isEmpty) {
|
|
||||||
_showError("Comment is required.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
isLoading.value = true;
|
|
||||||
final imageData = await _prepareImages(images, "task comment");
|
|
||||||
|
|
||||||
final success = await ApiService.commentTask(
|
|
||||||
id: projectId,
|
|
||||||
comment: commentField,
|
|
||||||
images: imageData,
|
|
||||||
).timeout(const Duration(seconds: 30), onTimeout: () {
|
|
||||||
logSafe("Task comment request timed out.", level: LogLevel.error);
|
|
||||||
throw Exception("Request timed out.");
|
|
||||||
});
|
|
||||||
|
|
||||||
if (success) {
|
|
||||||
_showSuccess("Task commented successfully!");
|
|
||||||
await taskController.fetchTaskData(projectId);
|
|
||||||
} else {
|
|
||||||
_showError("Failed to comment task.");
|
|
||||||
}
|
|
||||||
} catch (e, s) {
|
|
||||||
logSafe("Exception while commenting task", level: LogLevel.error, error: e, stackTrace: s);
|
|
||||||
_showError("An error occurred while commenting the task.");
|
|
||||||
} finally {
|
|
||||||
isLoading.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<List<Map<String, dynamic>>?> _prepareImages(List<File>? images, String context) async {
|
|
||||||
if (images == null || images.isEmpty) return null;
|
|
||||||
|
|
||||||
logSafe("Preparing images for $context upload...");
|
|
||||||
|
|
||||||
final results = await Future.wait(images.map((file) async {
|
|
||||||
try {
|
|
||||||
final compressed = await compressImageToUnder100KB(file);
|
|
||||||
if (compressed == null) return null;
|
|
||||||
|
|
||||||
return {
|
|
||||||
"fileName": file.path.split('/').last,
|
|
||||||
"base64Data": base64Encode(compressed),
|
|
||||||
"contentType": _getContentTypeFromFileName(file.path),
|
|
||||||
"fileSize": compressed.lengthInBytes,
|
|
||||||
"description": "Image uploaded for $context",
|
|
||||||
};
|
|
||||||
} catch (e) {
|
|
||||||
logSafe("Image processing failed: ${file.path}", level: LogLevel.warning, error: e);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
return results.whereType<Map<String, dynamic>>().toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
String _getContentTypeFromFileName(String fileName) {
|
|
||||||
final ext = fileName.split('.').last.toLowerCase();
|
|
||||||
return switch (ext) {
|
|
||||||
'jpg' || 'jpeg' => 'image/jpeg',
|
|
||||||
'png' => 'image/png',
|
|
||||||
'webp' => 'image/webp',
|
|
||||||
'gif' => 'image/gif',
|
|
||||||
_ => 'application/octet-stream',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> pickImages({required bool fromCamera}) async {
|
|
||||||
try {
|
|
||||||
if (fromCamera) {
|
|
||||||
final pickedFile = await _picker.pickImage(source: ImageSource.camera, imageQuality: 75);
|
|
||||||
if (pickedFile != null) {
|
|
||||||
selectedImages.add(File(pickedFile.path));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
final pickedFiles = await _picker.pickMultiImage(imageQuality: 75);
|
|
||||||
selectedImages.addAll(pickedFiles.map((xfile) => File(xfile.path)));
|
|
||||||
}
|
|
||||||
logSafe("Images picked: ${selectedImages.length}", );
|
|
||||||
} catch (e) {
|
|
||||||
logSafe("Error picking images", level: LogLevel.warning, error: e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void removeImageAt(int index) {
|
|
||||||
if (index >= 0 && index < selectedImages.length) {
|
|
||||||
selectedImages.removeAt(index);
|
|
||||||
logSafe("Removed image at index $index");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _showError(String message) => showAppSnackbar(
|
|
||||||
title: "Error",
|
|
||||||
message: message,
|
|
||||||
type: SnackbarType.error,
|
|
||||||
);
|
|
||||||
|
|
||||||
void _showSuccess(String message) => showAppSnackbar(
|
|
||||||
title: "Success",
|
|
||||||
message: message,
|
|
||||||
type: SnackbarType.success,
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,52 +0,0 @@
|
|||||||
import 'package:get/get.dart';
|
|
||||||
import 'package:marco/helpers/services/app_logger.dart';
|
|
||||||
import 'package:marco/helpers/services/api_service.dart';
|
|
||||||
import 'package:marco/model/attendance/organization_per_project_list_model.dart';
|
|
||||||
|
|
||||||
class OrganizationController extends GetxController {
|
|
||||||
/// List of organizations assigned to the selected project
|
|
||||||
List<Organization> organizations = [];
|
|
||||||
|
|
||||||
/// Currently selected organization (reactive)
|
|
||||||
Rxn<Organization> selectedOrganization = Rxn<Organization>();
|
|
||||||
|
|
||||||
/// Loading state for fetching organizations
|
|
||||||
final isLoadingOrganizations = false.obs;
|
|
||||||
|
|
||||||
/// Fetch organizations assigned to a given project
|
|
||||||
Future<void> fetchOrganizations(String projectId) async {
|
|
||||||
try {
|
|
||||||
isLoadingOrganizations.value = true;
|
|
||||||
|
|
||||||
final response = await ApiService.getAssignedOrganizations(projectId);
|
|
||||||
if (response != null && response.data.isNotEmpty) {
|
|
||||||
organizations = response.data;
|
|
||||||
logSafe("Organizations fetched: ${organizations.length}");
|
|
||||||
} else {
|
|
||||||
organizations = [];
|
|
||||||
logSafe("No organizations found for project $projectId",
|
|
||||||
level: LogLevel.warning);
|
|
||||||
}
|
|
||||||
} catch (e, stackTrace) {
|
|
||||||
logSafe("Failed to fetch organizations: $e",
|
|
||||||
level: LogLevel.error, error: e, stackTrace: stackTrace);
|
|
||||||
organizations = [];
|
|
||||||
} finally {
|
|
||||||
isLoadingOrganizations.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Select an organization
|
|
||||||
void selectOrganization(Organization? org) {
|
|
||||||
selectedOrganization.value = org;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Clear the selection (set to "All Organizations")
|
|
||||||
void clearSelection() {
|
|
||||||
selectedOrganization.value = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Current selection name for UI
|
|
||||||
String get currentSelection =>
|
|
||||||
selectedOrganization.value?.name ?? "All Organizations";
|
|
||||||
}
|
|
@ -1,43 +0,0 @@
|
|||||||
import 'package:get/get.dart';
|
|
||||||
import 'package:marco/helpers/services/api_service.dart';
|
|
||||||
import 'package:marco/helpers/services/app_logger.dart';
|
|
||||||
import 'package:marco/model/tenant/tenant_services_model.dart';
|
|
||||||
|
|
||||||
class ServiceController extends GetxController {
|
|
||||||
List<Service> services = [];
|
|
||||||
Service? selectedService;
|
|
||||||
final isLoadingServices = false.obs;
|
|
||||||
|
|
||||||
/// Fetch services assigned to a project
|
|
||||||
Future<void> fetchServices(String projectId) async {
|
|
||||||
try {
|
|
||||||
isLoadingServices.value = true;
|
|
||||||
final response = await ApiService.getAssignedServices(projectId);
|
|
||||||
if (response != null) {
|
|
||||||
services = response.data;
|
|
||||||
logSafe("Services fetched: ${services.length}");
|
|
||||||
} else {
|
|
||||||
logSafe("Failed to fetch services for project $projectId",
|
|
||||||
level: LogLevel.error);
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
isLoadingServices.value = false;
|
|
||||||
update();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Select a service
|
|
||||||
void selectService(Service? service) {
|
|
||||||
selectedService = service;
|
|
||||||
update();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Clear selection
|
|
||||||
void clearSelection() {
|
|
||||||
selectedService = null;
|
|
||||||
update();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Current selected name
|
|
||||||
String get currentSelection => selectedService?.name ?? "All Services";
|
|
||||||
}
|
|
@ -1,106 +0,0 @@
|
|||||||
import 'package:get/get.dart';
|
|
||||||
import 'package:marco/helpers/services/app_logger.dart';
|
|
||||||
import 'package:marco/helpers/services/tenant_service.dart';
|
|
||||||
import 'package:marco/model/tenant/tenant_list_model.dart';
|
|
||||||
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
|
||||||
import 'package:marco/helpers/services/storage/local_storage.dart';
|
|
||||||
|
|
||||||
class TenantSelectionController extends GetxController {
|
|
||||||
final TenantService _tenantService = TenantService();
|
|
||||||
|
|
||||||
var tenants = <Tenant>[].obs;
|
|
||||||
var isLoading = false.obs;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void onInit() {
|
|
||||||
super.onInit();
|
|
||||||
loadTenants();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Load tenants from API
|
|
||||||
Future<void> loadTenants({bool fromTenantSelectionScreen = false}) async {
|
|
||||||
try {
|
|
||||||
isLoading.value = true;
|
|
||||||
final data = await _tenantService.getTenants();
|
|
||||||
if (data != null) {
|
|
||||||
tenants.value = data.map((e) => Tenant.fromJson(e)).toList();
|
|
||||||
|
|
||||||
final recentTenantId = LocalStorage.getRecentTenantId();
|
|
||||||
|
|
||||||
// ✅ If user came from TenantSelectionScreen & recent tenant exists, auto-select
|
|
||||||
if (fromTenantSelectionScreen && recentTenantId != null) {
|
|
||||||
final tenantExists = tenants.any((t) => t.id == recentTenantId);
|
|
||||||
if (tenantExists) {
|
|
||||||
await onTenantSelected(recentTenantId);
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
// if tenant is no longer valid, clear recentTenant
|
|
||||||
await LocalStorage.removeRecentTenantId();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ✅ Auto-select if only one tenant
|
|
||||||
if (tenants.length == 1) {
|
|
||||||
await onTenantSelected(tenants.first.id);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
tenants.clear();
|
|
||||||
logSafe("⚠️ No tenants found for the user.", level: LogLevel.warning);
|
|
||||||
}
|
|
||||||
} catch (e, st) {
|
|
||||||
logSafe("❌ Exception in loadTenants",
|
|
||||||
level: LogLevel.error, error: e, stackTrace: st);
|
|
||||||
} finally {
|
|
||||||
isLoading.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Select tenant
|
|
||||||
Future<void> onTenantSelected(String tenantId) async {
|
|
||||||
try {
|
|
||||||
isLoading.value = true;
|
|
||||||
final success = await _tenantService.selectTenant(tenantId);
|
|
||||||
if (success) {
|
|
||||||
logSafe("✅ Tenant selection successful: $tenantId");
|
|
||||||
|
|
||||||
// Store selected tenant in memory
|
|
||||||
final selectedTenant = tenants.firstWhere((t) => t.id == tenantId);
|
|
||||||
TenantService.setSelectedTenant(selectedTenant);
|
|
||||||
|
|
||||||
// 🔥 Save in LocalStorage
|
|
||||||
await LocalStorage.setRecentTenantId(tenantId);
|
|
||||||
|
|
||||||
// Navigate to dashboard
|
|
||||||
Get.offAllNamed('/dashboard');
|
|
||||||
|
|
||||||
showAppSnackbar(
|
|
||||||
title: "Success",
|
|
||||||
message: "Organization selected successfully.",
|
|
||||||
type: SnackbarType.success,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
logSafe("❌ Tenant selection failed for: $tenantId",
|
|
||||||
level: LogLevel.warning);
|
|
||||||
|
|
||||||
// Show error snackbar
|
|
||||||
showAppSnackbar(
|
|
||||||
title: "Error",
|
|
||||||
message: "Unable to select organization. Please try again.",
|
|
||||||
type: SnackbarType.error,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (e, st) {
|
|
||||||
logSafe("❌ Exception in onTenantSelected",
|
|
||||||
level: LogLevel.error, error: e, stackTrace: st);
|
|
||||||
|
|
||||||
// Show error snackbar for exception
|
|
||||||
showAppSnackbar(
|
|
||||||
title: "Error",
|
|
||||||
message: "An unexpected error occurred while selecting organization.",
|
|
||||||
type: SnackbarType.error,
|
|
||||||
);
|
|
||||||
} finally {
|
|
||||||
isLoading.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -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_details_model.dart';
|
||||||
import 'package:marco/model/document/document_version_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/attendance/organization_per_project_list_model.dart';
|
||||||
import 'package:marco/model/tenant/tenant_services_model.dart';
|
|
||||||
|
|
||||||
class ApiService {
|
class ApiService {
|
||||||
static const bool enableLogs = true;
|
static const bool enableLogs = true;
|
||||||
@ -319,36 +318,6 @@ class ApiService {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
//// Get Services assigned to a Project
|
|
||||||
static Future<ServiceListResponse?> getAssignedServices(
|
|
||||||
String projectId) async {
|
|
||||||
final endpoint = "${ApiEndpoints.getAssignedServices}/$projectId";
|
|
||||||
logSafe("Fetching services assigned to projectId: $projectId");
|
|
||||||
|
|
||||||
try {
|
|
||||||
final response = await _getRequest(endpoint);
|
|
||||||
|
|
||||||
if (response == null) {
|
|
||||||
logSafe("Assigned Services request failed: null response",
|
|
||||||
level: LogLevel.error);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
final jsonResponse =
|
|
||||||
_parseResponseForAllData(response, label: "Assigned Services");
|
|
||||||
|
|
||||||
if (jsonResponse != null) {
|
|
||||||
return ServiceListResponse.fromJson(jsonResponse);
|
|
||||||
}
|
|
||||||
} catch (e, stack) {
|
|
||||||
logSafe("Exception during getAssignedServices: $e",
|
|
||||||
level: LogLevel.error);
|
|
||||||
logSafe("StackTrace: $stack", level: LogLevel.debug);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
static Future<bool> postLogsApi(List<Map<String, dynamic>> logs) async {
|
static Future<bool> postLogsApi(List<Map<String, dynamic>> logs) async {
|
||||||
const endpoint = "${ApiEndpoints.uploadLogs}";
|
const endpoint = "${ApiEndpoints.uploadLogs}";
|
||||||
logSafe("Posting logs... count=${logs.length}");
|
logSafe("Posting logs... count=${logs.length}");
|
||||||
|
@ -1,9 +1,6 @@
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:logger/logger.dart';
|
import 'package:logger/logger.dart';
|
||||||
|
|
||||||
import 'package:marco/controller/attendance/attendance_screen_controller.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_screen_controller.dart';
|
||||||
import 'package:marco/controller/expense/expense_detail_controller.dart';
|
import 'package:marco/controller/expense/expense_detail_controller.dart';
|
||||||
import 'package:marco/controller/directory/directory_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
|
// Call method to handle team modifications and dashboard update
|
||||||
_handleDashboardUpdate(data);
|
_handleDashboardUpdate(data);
|
||||||
break;
|
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
|
/// 🔹 Expenses
|
||||||
case 'Expenses_Modified':
|
case 'Expenses_Modified':
|
||||||
_handleExpenseUpdated(data);
|
_handleExpenseUpdated(data);
|
||||||
@ -128,23 +105,7 @@ class NotificationActionHandler {
|
|||||||
|
|
||||||
/// ---------------------- HANDLERS ----------------------
|
/// ---------------------- HANDLERS ----------------------
|
||||||
|
|
||||||
static void _handleTaskPlanningUpdated(Map<String, dynamic> data) {
|
|
||||||
final projectId = data['ProjectId'];
|
|
||||||
if (projectId == null) {
|
|
||||||
_logger.w("⚠️ TaskPlanning update received without ProjectId: $data");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_safeControllerUpdate<DailyTaskPlanningController>(
|
|
||||||
onFound: (controller) {
|
|
||||||
controller.fetchTaskData(projectId);
|
|
||||||
},
|
|
||||||
notFoundMessage:
|
|
||||||
'⚠️ DailyTaskPlanningController not found, cannot refresh.',
|
|
||||||
successMessage:
|
|
||||||
'✅ DailyTaskPlanningController refreshed from notification.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool _isAttendanceAction(String? action) {
|
static bool _isAttendanceAction(String? action) {
|
||||||
const validActions = {
|
const validActions = {
|
||||||
@ -199,18 +160,6 @@ class NotificationActionHandler {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void _handleTaskUpdated(Map<String, dynamic> data,
|
|
||||||
{required bool isComment}) {
|
|
||||||
_safeControllerUpdate<DailyTaskController>(
|
|
||||||
onFound: (controller) => controller.refreshTasksFromNotification(
|
|
||||||
projectId: data['ProjectId'],
|
|
||||||
taskAllocationId: data['TaskAllocationId'],
|
|
||||||
),
|
|
||||||
notFoundMessage: '⚠️ DailyTaskController not found, cannot update.',
|
|
||||||
successMessage: '✅ DailyTaskController refreshed from notification.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// ---------------------- DOCUMENT HANDLER ----------------------
|
/// ---------------------- DOCUMENT HANDLER ----------------------
|
||||||
static void _handleDocumentModified(Map<String, dynamic> data) {
|
static void _handleDocumentModified(Map<String, dynamic> data) {
|
||||||
String entityTypeId;
|
String entityTypeId;
|
||||||
|
@ -1,152 +0,0 @@
|
|||||||
import 'dart:convert';
|
|
||||||
import 'package:http/http.dart' as http;
|
|
||||||
import 'package:get/get.dart';
|
|
||||||
import 'package:marco/controller/project_controller.dart';
|
|
||||||
|
|
||||||
import 'package:marco/helpers/services/api_endpoints.dart';
|
|
||||||
import 'package:marco/helpers/services/storage/local_storage.dart';
|
|
||||||
import 'package:marco/helpers/services/app_logger.dart';
|
|
||||||
import 'package:marco/helpers/services/auth_service.dart';
|
|
||||||
import 'package:marco/model/tenant/tenant_list_model.dart';
|
|
||||||
|
|
||||||
/// Abstract interface for tenant service functionality
|
|
||||||
abstract class ITenantService {
|
|
||||||
Future<List<Map<String, dynamic>>?> getTenants({bool hasRetried = false});
|
|
||||||
Future<bool> selectTenant(String tenantId, {bool hasRetried = false});
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Tenant API service
|
|
||||||
class TenantService implements ITenantService {
|
|
||||||
static const String _baseUrl = ApiEndpoints.baseUrl;
|
|
||||||
static const Map<String, String> _headers = {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Currently selected tenant
|
|
||||||
static Tenant? currentTenant;
|
|
||||||
|
|
||||||
/// Set the selected tenant
|
|
||||||
static void setSelectedTenant(Tenant tenant) {
|
|
||||||
currentTenant = tenant;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check if tenant is selected
|
|
||||||
static bool get isTenantSelected => currentTenant != null;
|
|
||||||
|
|
||||||
/// Build authorized headers
|
|
||||||
static Future<Map<String, String>> _authorizedHeaders() async {
|
|
||||||
final token = await LocalStorage.getJwtToken();
|
|
||||||
if (token == null || token.isEmpty) {
|
|
||||||
throw Exception('Missing JWT token');
|
|
||||||
}
|
|
||||||
return {..._headers, 'Authorization': 'Bearer $token'};
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Handle API errors
|
|
||||||
static void _handleApiError(
|
|
||||||
http.Response response, dynamic data, String context) {
|
|
||||||
final message = data['message'] ?? 'Unknown error';
|
|
||||||
final level =
|
|
||||||
response.statusCode >= 500 ? LogLevel.error : LogLevel.warning;
|
|
||||||
logSafe("❌ $context failed: $message [Status: ${response.statusCode}]",
|
|
||||||
level: level);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Log exceptions
|
|
||||||
static void _logException(dynamic e, dynamic st, String context) {
|
|
||||||
logSafe("❌ $context exception",
|
|
||||||
level: LogLevel.error, error: e, stackTrace: st);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<List<Map<String, dynamic>>?> getTenants(
|
|
||||||
{bool hasRetried = false}) async {
|
|
||||||
try {
|
|
||||||
final headers = await _authorizedHeaders();
|
|
||||||
logSafe("➡️ GET $_baseUrl/auth/get/user/tenants\nHeaders: $headers",
|
|
||||||
level: LogLevel.info);
|
|
||||||
|
|
||||||
final response = await http
|
|
||||||
.get(Uri.parse("$_baseUrl/auth/get/user/tenants"), headers: headers);
|
|
||||||
final data = jsonDecode(response.body);
|
|
||||||
|
|
||||||
logSafe(
|
|
||||||
"⬅️ Response: ${jsonEncode(data)} [Status: ${response.statusCode}]",
|
|
||||||
level: LogLevel.info);
|
|
||||||
|
|
||||||
if (response.statusCode == 200 && data['success'] == true) {
|
|
||||||
logSafe("✅ Tenants fetched successfully.");
|
|
||||||
return List<Map<String, dynamic>>.from(data['data']);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.statusCode == 401 && !hasRetried) {
|
|
||||||
logSafe("⚠️ Unauthorized while fetching tenants. Refreshing token...",
|
|
||||||
level: LogLevel.warning);
|
|
||||||
final refreshed = await AuthService.refreshToken();
|
|
||||||
if (refreshed) return getTenants(hasRetried: true);
|
|
||||||
logSafe("❌ Token refresh failed while fetching tenants.",
|
|
||||||
level: LogLevel.error);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
_handleApiError(response, data, "Fetching tenants");
|
|
||||||
return null;
|
|
||||||
} catch (e, st) {
|
|
||||||
_logException(e, st, "Get Tenants API");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Future<bool> selectTenant(String tenantId, {bool hasRetried = false}) async {
|
|
||||||
try {
|
|
||||||
final headers = await _authorizedHeaders();
|
|
||||||
logSafe(
|
|
||||||
"➡️ POST $_baseUrl/auth/select-tenant/$tenantId\nHeaders: $headers",
|
|
||||||
level: LogLevel.info);
|
|
||||||
|
|
||||||
final response = await http.post(
|
|
||||||
Uri.parse("$_baseUrl/auth/select-tenant/$tenantId"),
|
|
||||||
headers: headers,
|
|
||||||
);
|
|
||||||
final data = jsonDecode(response.body);
|
|
||||||
|
|
||||||
logSafe(
|
|
||||||
"⬅️ Response: ${jsonEncode(data)} [Status: ${response.statusCode}]",
|
|
||||||
level: LogLevel.info);
|
|
||||||
|
|
||||||
if (response.statusCode == 200 && data['success'] == true) {
|
|
||||||
await LocalStorage.setJwtToken(data['data']['token']);
|
|
||||||
await LocalStorage.setRefreshToken(data['data']['refreshToken']);
|
|
||||||
logSafe("✅ Tenant selected successfully. Tokens updated.");
|
|
||||||
|
|
||||||
// 🔥 Refresh projects when tenant changes
|
|
||||||
try {
|
|
||||||
final projectController = Get.find<ProjectController>();
|
|
||||||
projectController.clearProjects();
|
|
||||||
projectController.fetchProjects();
|
|
||||||
} catch (_) {
|
|
||||||
logSafe("⚠️ ProjectController not found while refreshing projects");
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.statusCode == 401 && !hasRetried) {
|
|
||||||
logSafe("⚠️ Unauthorized while selecting tenant. Refreshing token...",
|
|
||||||
level: LogLevel.warning);
|
|
||||||
final refreshed = await AuthService.refreshToken();
|
|
||||||
if (refreshed) return selectTenant(tenantId, hasRetried: true);
|
|
||||||
logSafe("❌ Token refresh failed while selecting tenant.",
|
|
||||||
level: LogLevel.error);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
_handleApiError(response, data, "Selecting tenant");
|
|
||||||
return false;
|
|
||||||
} catch (e, st) {
|
|
||||||
_logException(e, st, "Select Tenant API");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,106 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:get/get.dart';
|
|
||||||
import 'package:marco/controller/tenant/organization_selection_controller.dart';
|
|
||||||
import 'package:marco/helpers/widgets/my_text.dart';
|
|
||||||
import 'package:marco/model/attendance/organization_per_project_list_model.dart';
|
|
||||||
|
|
||||||
class OrganizationSelector extends StatelessWidget {
|
|
||||||
final OrganizationController controller;
|
|
||||||
|
|
||||||
/// Called whenever a new organization is selected (including "All Organizations").
|
|
||||||
final Future<void> Function(Organization?)? onSelectionChanged;
|
|
||||||
|
|
||||||
/// Optional height for the selector. If null, uses default padding-based height.
|
|
||||||
final double? height;
|
|
||||||
|
|
||||||
const OrganizationSelector({
|
|
||||||
super.key,
|
|
||||||
required this.controller,
|
|
||||||
this.onSelectionChanged,
|
|
||||||
this.height,
|
|
||||||
});
|
|
||||||
|
|
||||||
Widget _popupSelector({
|
|
||||||
required String currentValue,
|
|
||||||
required List<String> items,
|
|
||||||
}) {
|
|
||||||
return PopupMenuButton<String>(
|
|
||||||
color: Colors.white,
|
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
|
||||||
onSelected: (name) async {
|
|
||||||
Organization? org = name == "All Organizations"
|
|
||||||
? null
|
|
||||||
: controller.organizations.firstWhere((e) => e.name == name);
|
|
||||||
|
|
||||||
controller.selectOrganization(org);
|
|
||||||
|
|
||||||
if (onSelectionChanged != null) {
|
|
||||||
await onSelectionChanged!(org);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
itemBuilder: (context) => items
|
|
||||||
.map((e) => PopupMenuItem<String>(value: e, child: MyText(e)))
|
|
||||||
.toList(),
|
|
||||||
child: Container(
|
|
||||||
height: height,
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.white,
|
|
||||||
border: Border.all(color: Colors.grey.shade300),
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
child: Center(
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
currentValue,
|
|
||||||
style: const TextStyle(
|
|
||||||
color: Colors.black87,
|
|
||||||
fontSize: 13,
|
|
||||||
height: 1.2,
|
|
||||||
),
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const Icon(Icons.arrow_drop_down, color: Colors.grey, size: 18),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Obx(() {
|
|
||||||
if (controller.isLoadingOrganizations.value) {
|
|
||||||
return const Center(child: CircularProgressIndicator());
|
|
||||||
} else if (controller.organizations.isEmpty) {
|
|
||||||
return Center(
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 16.0),
|
|
||||||
child: MyText.bodyMedium(
|
|
||||||
"No organizations found",
|
|
||||||
fontWeight: 500,
|
|
||||||
color: Colors.grey,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
final orgNames = [
|
|
||||||
"All Organizations",
|
|
||||||
...controller.organizations.map((e) => e.name)
|
|
||||||
];
|
|
||||||
|
|
||||||
// Listen to selectedOrganization.value
|
|
||||||
return _popupSelector(
|
|
||||||
currentValue: controller.currentSelection,
|
|
||||||
items: orgNames,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,114 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:get/get.dart';
|
|
||||||
import 'package:marco/helpers/widgets/my_text.dart';
|
|
||||||
import 'package:marco/model/tenant/tenant_services_model.dart';
|
|
||||||
import 'package:marco/controller/tenant/service_controller.dart';
|
|
||||||
|
|
||||||
class ServiceSelector extends StatelessWidget {
|
|
||||||
final ServiceController controller;
|
|
||||||
|
|
||||||
/// Called whenever a new service is selected (including "All Services")
|
|
||||||
final Future<void> Function(Service?)? onSelectionChanged;
|
|
||||||
|
|
||||||
/// Optional height for the selector
|
|
||||||
final double? height;
|
|
||||||
|
|
||||||
const ServiceSelector({
|
|
||||||
super.key,
|
|
||||||
required this.controller,
|
|
||||||
this.onSelectionChanged,
|
|
||||||
this.height,
|
|
||||||
});
|
|
||||||
|
|
||||||
Widget _popupSelector({
|
|
||||||
required String currentValue,
|
|
||||||
required List<String> items,
|
|
||||||
}) {
|
|
||||||
return PopupMenuButton<String>(
|
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
|
||||||
onSelected: items.isEmpty
|
|
||||||
? null
|
|
||||||
: (name) async {
|
|
||||||
Service? service = name == "All Services"
|
|
||||||
? null
|
|
||||||
: controller.services.firstWhere((e) => e.name == name);
|
|
||||||
|
|
||||||
controller.selectService(service);
|
|
||||||
|
|
||||||
if (onSelectionChanged != null) {
|
|
||||||
await onSelectionChanged!(service);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
itemBuilder: (context) {
|
|
||||||
if (items.isEmpty || items.length == 1 && items[0] == "All Services") {
|
|
||||||
return [
|
|
||||||
const PopupMenuItem<String>(
|
|
||||||
enabled: false,
|
|
||||||
child: Center(
|
|
||||||
child: Text(
|
|
||||||
"No services found",
|
|
||||||
style: TextStyle(color: Colors.grey),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
return items
|
|
||||||
.map((e) => PopupMenuItem<String>(value: e, child: MyText(e)))
|
|
||||||
.toList();
|
|
||||||
},
|
|
||||||
child: Container(
|
|
||||||
height: height,
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.grey.shade100,
|
|
||||||
border: Border.all(color: Colors.grey.shade300),
|
|
||||||
borderRadius: BorderRadius.circular(10),
|
|
||||||
),
|
|
||||||
child: Center(
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
currentValue.isEmpty ? "No services found" : currentValue,
|
|
||||||
style: const TextStyle(
|
|
||||||
color: Colors.black87,
|
|
||||||
fontSize: 13,
|
|
||||||
height: 1.2,
|
|
||||||
),
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const Icon(Icons.arrow_drop_down, color: Colors.grey, size: 18),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Obx(() {
|
|
||||||
if (controller.isLoadingServices.value) {
|
|
||||||
return const Center(child: CircularProgressIndicator());
|
|
||||||
}
|
|
||||||
|
|
||||||
final serviceNames = controller.services.isEmpty
|
|
||||||
? <String>[]
|
|
||||||
: <String>[
|
|
||||||
"All Services",
|
|
||||||
...controller.services.map((e) => e.name).toList(),
|
|
||||||
];
|
|
||||||
|
|
||||||
final currentValue =
|
|
||||||
controller.services.isEmpty ? "" : controller.currentSelection;
|
|
||||||
|
|
||||||
return _popupSelector(
|
|
||||||
currentValue: currentValue,
|
|
||||||
items: serviceNames,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,368 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:get/get.dart';
|
|
||||||
import 'package:marco/controller/task_Planning/daily_task_Planning_controller.dart';
|
|
||||||
import 'package:marco/controller/project_controller.dart';
|
|
||||||
import 'package:marco/helpers/widgets/my_spacing.dart';
|
|
||||||
import 'package:marco/helpers/widgets/my_text.dart';
|
|
||||||
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
|
||||||
import 'package:marco/helpers/utils/base_bottom_sheet.dart';
|
|
||||||
|
|
||||||
class AssignTaskBottomSheet extends StatefulWidget {
|
|
||||||
final String workLocation;
|
|
||||||
final String activityName;
|
|
||||||
final int pendingTask;
|
|
||||||
final String workItemId;
|
|
||||||
final DateTime assignmentDate;
|
|
||||||
final String buildingName;
|
|
||||||
final String floorName;
|
|
||||||
final String workAreaName;
|
|
||||||
|
|
||||||
const AssignTaskBottomSheet({
|
|
||||||
super.key,
|
|
||||||
required this.buildingName,
|
|
||||||
required this.workLocation,
|
|
||||||
required this.floorName,
|
|
||||||
required this.workAreaName,
|
|
||||||
required this.activityName,
|
|
||||||
required this.pendingTask,
|
|
||||||
required this.workItemId,
|
|
||||||
required this.assignmentDate,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<AssignTaskBottomSheet> createState() => _AssignTaskBottomSheetState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _AssignTaskBottomSheetState extends State<AssignTaskBottomSheet> {
|
|
||||||
final DailyTaskPlanningController controller = Get.find();
|
|
||||||
final ProjectController projectController = Get.find();
|
|
||||||
final TextEditingController targetController = TextEditingController();
|
|
||||||
final TextEditingController descriptionController = TextEditingController();
|
|
||||||
final ScrollController _employeeListScrollController = ScrollController();
|
|
||||||
|
|
||||||
String? selectedProjectId;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
selectedProjectId = projectController.selectedProjectId.value;
|
|
||||||
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
||||||
if (selectedProjectId != null) {
|
|
||||||
controller.fetchEmployeesByProject(selectedProjectId!);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_employeeListScrollController.dispose();
|
|
||||||
targetController.dispose();
|
|
||||||
descriptionController.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Obx(() => BaseBottomSheet(
|
|
||||||
title: "Assign Task",
|
|
||||||
child: _buildAssignTaskForm(),
|
|
||||||
onCancel: () => Get.back(),
|
|
||||||
onSubmit: _onAssignTaskPressed,
|
|
||||||
isSubmitting: controller.isAssigningTask.value,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildAssignTaskForm() {
|
|
||||||
return Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
_infoRow(Icons.location_on, "Work Location",
|
|
||||||
"${widget.buildingName} > ${widget.floorName} > ${widget.workAreaName} > ${widget.activityName}"),
|
|
||||||
Divider(),
|
|
||||||
_infoRow(Icons.pending_actions, "Pending Task of Activity",
|
|
||||||
"${widget.pendingTask}"),
|
|
||||||
Divider(),
|
|
||||||
GestureDetector(
|
|
||||||
onTap: _onRoleMenuPressed,
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
MyText.titleMedium("Select Team :", fontWeight: 600),
|
|
||||||
const SizedBox(width: 4),
|
|
||||||
const Icon(Icons.tune, color: Color.fromARGB(255, 95, 132, 255)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
MySpacing.height(8),
|
|
||||||
Container(
|
|
||||||
constraints: const BoxConstraints(maxHeight: 150),
|
|
||||||
child: _buildEmployeeList(),
|
|
||||||
),
|
|
||||||
MySpacing.height(8),
|
|
||||||
_buildSelectedEmployees(),
|
|
||||||
_buildTextField(
|
|
||||||
icon: Icons.track_changes,
|
|
||||||
label: "Target for Today :",
|
|
||||||
controller: targetController,
|
|
||||||
hintText: "Enter target",
|
|
||||||
keyboardType: TextInputType.number,
|
|
||||||
validatorType: "target",
|
|
||||||
),
|
|
||||||
MySpacing.height(24),
|
|
||||||
_buildTextField(
|
|
||||||
icon: Icons.description,
|
|
||||||
label: "Description :",
|
|
||||||
controller: descriptionController,
|
|
||||||
hintText: "Enter task description",
|
|
||||||
maxLines: 3,
|
|
||||||
validatorType: "description",
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _onRoleMenuPressed() {
|
|
||||||
final RenderBox overlay =
|
|
||||||
Overlay.of(context).context.findRenderObject() as RenderBox;
|
|
||||||
final Size screenSize = overlay.size;
|
|
||||||
|
|
||||||
showMenu(
|
|
||||||
context: context,
|
|
||||||
position: RelativeRect.fromLTRB(
|
|
||||||
screenSize.width / 2 - 100,
|
|
||||||
screenSize.height / 2 - 20,
|
|
||||||
screenSize.width / 2 - 100,
|
|
||||||
screenSize.height / 2 - 20,
|
|
||||||
),
|
|
||||||
items: [
|
|
||||||
const PopupMenuItem(value: 'all', child: Text("All Roles")),
|
|
||||||
...controller.roles.map((role) {
|
|
||||||
return PopupMenuItem(
|
|
||||||
value: role['id'].toString(),
|
|
||||||
child: Text(role['name'] ?? 'Unknown Role'),
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
).then((value) {
|
|
||||||
if (value != null) {
|
|
||||||
controller.onRoleSelected(value == 'all' ? null : value);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildEmployeeList() {
|
|
||||||
return Obx(() {
|
|
||||||
if (controller.isLoading.value) {
|
|
||||||
return const Center(child: CircularProgressIndicator());
|
|
||||||
}
|
|
||||||
|
|
||||||
final selectedRoleId = controller.selectedRoleId.value;
|
|
||||||
final filteredEmployees = selectedRoleId == null
|
|
||||||
? controller.employees
|
|
||||||
: controller.employees
|
|
||||||
.where((e) => e.jobRoleID.toString() == selectedRoleId)
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
if (filteredEmployees.isEmpty) {
|
|
||||||
return const Text("No employees found for selected role.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return Scrollbar(
|
|
||||||
controller: _employeeListScrollController,
|
|
||||||
thumbVisibility: true,
|
|
||||||
child: ListView.builder(
|
|
||||||
controller: _employeeListScrollController,
|
|
||||||
shrinkWrap: true,
|
|
||||||
itemCount: filteredEmployees.length,
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
final employee = filteredEmployees[index];
|
|
||||||
final rxBool = controller.uploadingStates[employee.id];
|
|
||||||
|
|
||||||
return Obx(() => Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 2),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Checkbox(
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(4),
|
|
||||||
),
|
|
||||||
value: rxBool?.value ?? false,
|
|
||||||
onChanged: (bool? selected) {
|
|
||||||
if (rxBool != null) {
|
|
||||||
rxBool.value = selected ?? false;
|
|
||||||
controller.updateSelectedEmployees();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
fillColor:
|
|
||||||
WidgetStateProperty.resolveWith<Color>((states) {
|
|
||||||
if (states.contains(WidgetState.selected)) {
|
|
||||||
return const Color.fromARGB(255, 95, 132, 255);
|
|
||||||
}
|
|
||||||
return Colors.transparent;
|
|
||||||
}),
|
|
||||||
checkColor: Colors.white,
|
|
||||||
side: const BorderSide(color: Colors.black),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
Expanded(
|
|
||||||
child: Text(employee.name,
|
|
||||||
style: const TextStyle(fontSize: 14))),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
));
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildSelectedEmployees() {
|
|
||||||
return Obx(() {
|
|
||||||
if (controller.selectedEmployees.isEmpty) return Container();
|
|
||||||
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
|
||||||
child: Wrap(
|
|
||||||
spacing: 4,
|
|
||||||
runSpacing: 4,
|
|
||||||
children: controller.selectedEmployees.map((e) {
|
|
||||||
return Obx(() {
|
|
||||||
final isSelected =
|
|
||||||
controller.uploadingStates[e.id]?.value ?? false;
|
|
||||||
if (!isSelected) return Container();
|
|
||||||
|
|
||||||
return Chip(
|
|
||||||
label:
|
|
||||||
Text(e.name, style: const TextStyle(color: Colors.white)),
|
|
||||||
backgroundColor: const Color.fromARGB(255, 95, 132, 255),
|
|
||||||
deleteIcon: const Icon(Icons.close, color: Colors.white),
|
|
||||||
onDeleted: () {
|
|
||||||
controller.uploadingStates[e.id]?.value = false;
|
|
||||||
controller.updateSelectedEmployees();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}).toList(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildTextField({
|
|
||||||
required IconData icon,
|
|
||||||
required String label,
|
|
||||||
required TextEditingController controller,
|
|
||||||
required String hintText,
|
|
||||||
TextInputType keyboardType = TextInputType.text,
|
|
||||||
int maxLines = 1,
|
|
||||||
required String validatorType,
|
|
||||||
}) {
|
|
||||||
return Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Icon(icon, size: 18, color: Colors.black54),
|
|
||||||
const SizedBox(width: 6),
|
|
||||||
MyText.titleMedium(label, fontWeight: 600),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
MySpacing.height(6),
|
|
||||||
TextFormField(
|
|
||||||
controller: controller,
|
|
||||||
keyboardType: keyboardType,
|
|
||||||
maxLines: maxLines,
|
|
||||||
decoration: const InputDecoration(
|
|
||||||
hintText: '',
|
|
||||||
border: OutlineInputBorder(),
|
|
||||||
),
|
|
||||||
validator: (value) =>
|
|
||||||
this.controller.formFieldValidator(value, fieldType: validatorType),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _infoRow(IconData icon, String title, String value) {
|
|
||||||
return Padding(
|
|
||||||
padding: MySpacing.y(6),
|
|
||||||
child: Row(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Icon(icon, size: 20, color: Colors.grey[700]),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
Expanded(
|
|
||||||
child: RichText(
|
|
||||||
text: TextSpan(
|
|
||||||
children: [
|
|
||||||
WidgetSpan(
|
|
||||||
child: MyText.titleMedium("$title: ",
|
|
||||||
fontWeight: 600, color: Colors.black),
|
|
||||||
),
|
|
||||||
TextSpan(
|
|
||||||
text: value,
|
|
||||||
style: const TextStyle(color: Colors.black),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _onAssignTaskPressed() {
|
|
||||||
final selectedTeam = controller.uploadingStates.entries
|
|
||||||
.where((e) => e.value.value)
|
|
||||||
.map((e) => e.key)
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
if (selectedTeam.isEmpty) {
|
|
||||||
showAppSnackbar(
|
|
||||||
title: "Team Required",
|
|
||||||
message: "Please select at least one team member",
|
|
||||||
type: SnackbarType.error,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final target = int.tryParse(targetController.text.trim());
|
|
||||||
if (target == null || target <= 0) {
|
|
||||||
showAppSnackbar(
|
|
||||||
title: "Invalid Input",
|
|
||||||
message: "Please enter a valid target number",
|
|
||||||
type: SnackbarType.error,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (target > widget.pendingTask) {
|
|
||||||
showAppSnackbar(
|
|
||||||
title: "Target Too High",
|
|
||||||
message:
|
|
||||||
"Target cannot be greater than pending task (${widget.pendingTask})",
|
|
||||||
type: SnackbarType.error,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final description = descriptionController.text.trim();
|
|
||||||
if (description.isEmpty) {
|
|
||||||
showAppSnackbar(
|
|
||||||
title: "Description Required",
|
|
||||||
message: "Please enter a description",
|
|
||||||
type: SnackbarType.error,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
controller.assignDailyTask(
|
|
||||||
workItemId: widget.workItemId,
|
|
||||||
plannedTask: target,
|
|
||||||
description: description,
|
|
||||||
taskTeam: selectedTeam,
|
|
||||||
assignmentDate: widget.assignmentDate,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,678 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:get/get.dart';
|
|
||||||
import 'package:intl/intl.dart';
|
|
||||||
import 'dart:io';
|
|
||||||
import 'dart:math' as math;
|
|
||||||
// --- Assumed Imports (ensure these paths are correct in your project) ---
|
|
||||||
import 'package:marco/controller/task_planning/report_task_controller.dart';
|
|
||||||
import 'package:marco/helpers/utils/mixins/ui_mixin.dart';
|
|
||||||
import 'package:marco/helpers/widgets/my_button.dart';
|
|
||||||
import 'package:marco/helpers/widgets/my_spacing.dart';
|
|
||||||
import 'package:marco/helpers/widgets/my_text.dart';
|
|
||||||
import 'package:marco/helpers/widgets/my_text_style.dart';
|
|
||||||
import 'package:marco/helpers/widgets/avatar.dart';
|
|
||||||
import 'package:marco/helpers/widgets/my_team_model_sheet.dart';
|
|
||||||
import 'package:marco/helpers/widgets/image_viewer_dialog.dart';
|
|
||||||
import 'package:marco/model/dailyTaskPlanning/create_task_botom_sheet.dart';
|
|
||||||
import 'package:marco/helpers/utils/base_bottom_sheet.dart';
|
|
||||||
|
|
||||||
// --- Form Field Keys (Unchanged) ---
|
|
||||||
class _FormFieldKeys {
|
|
||||||
static const String assignedDate = 'assigned_date';
|
|
||||||
static const String assignedBy = 'assigned_by';
|
|
||||||
static const String workArea = 'work_area';
|
|
||||||
static const String activity = 'activity';
|
|
||||||
static const String plannedWork = 'planned_work';
|
|
||||||
static const String completedWork = 'completed_work';
|
|
||||||
static const String teamMembers = 'team_members';
|
|
||||||
static const String assigned = 'assigned';
|
|
||||||
static const String taskId = 'task_id';
|
|
||||||
static const String comment = 'comment';
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Main Widget: CommentTaskBottomSheet ---
|
|
||||||
class CommentTaskBottomSheet extends StatefulWidget {
|
|
||||||
final Map<String, dynamic> taskData;
|
|
||||||
final VoidCallback? onCommentSuccess;
|
|
||||||
final String taskDataId;
|
|
||||||
final String workAreaId;
|
|
||||||
final String activityId;
|
|
||||||
|
|
||||||
const CommentTaskBottomSheet({
|
|
||||||
super.key,
|
|
||||||
required this.taskData,
|
|
||||||
this.onCommentSuccess,
|
|
||||||
required this.taskDataId,
|
|
||||||
required this.workAreaId,
|
|
||||||
required this.activityId,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<CommentTaskBottomSheet> createState() => _CommentTaskBottomSheetState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _Member {
|
|
||||||
final String firstName;
|
|
||||||
_Member(this.firstName);
|
|
||||||
}
|
|
||||||
|
|
||||||
class _CommentTaskBottomSheetState extends State<CommentTaskBottomSheet>
|
|
||||||
with UIMixin {
|
|
||||||
late final ReportTaskController controller;
|
|
||||||
List<Map<String, dynamic>> _sortedComments = [];
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
controller = Get.put(ReportTaskController(),
|
|
||||||
tag: widget.taskData['taskId'] ?? UniqueKey().toString());
|
|
||||||
_initializeControllerData();
|
|
||||||
|
|
||||||
final comments = List<Map<String, dynamic>>.from(
|
|
||||||
widget.taskData['taskComments'] as List? ?? []);
|
|
||||||
comments.sort((a, b) {
|
|
||||||
final aDate = DateTime.tryParse(a['date'] ?? '') ??
|
|
||||||
DateTime.fromMillisecondsSinceEpoch(0);
|
|
||||||
final bDate = DateTime.tryParse(b['date'] ?? '') ??
|
|
||||||
DateTime.fromMillisecondsSinceEpoch(0);
|
|
||||||
return bDate.compareTo(aDate); // Newest first
|
|
||||||
});
|
|
||||||
_sortedComments = comments;
|
|
||||||
}
|
|
||||||
|
|
||||||
void _initializeControllerData() {
|
|
||||||
final data = widget.taskData;
|
|
||||||
|
|
||||||
final fieldMappings = {
|
|
||||||
_FormFieldKeys.assignedDate: data['assignedOn'],
|
|
||||||
_FormFieldKeys.assignedBy: data['assignedBy'],
|
|
||||||
_FormFieldKeys.workArea: data['location'],
|
|
||||||
_FormFieldKeys.activity: data['activity'],
|
|
||||||
_FormFieldKeys.plannedWork: data['plannedWork'],
|
|
||||||
_FormFieldKeys.completedWork: data['completedWork'],
|
|
||||||
_FormFieldKeys.teamMembers: (data['teamMembers'] as List?)?.join(', '),
|
|
||||||
_FormFieldKeys.assigned: data['assigned'],
|
|
||||||
_FormFieldKeys.taskId: data['taskId'],
|
|
||||||
};
|
|
||||||
|
|
||||||
for (final entry in fieldMappings.entries) {
|
|
||||||
controller.basicValidator.getController(entry.key)?.text =
|
|
||||||
entry.value ?? '';
|
|
||||||
}
|
|
||||||
|
|
||||||
controller.basicValidator.getController(_FormFieldKeys.comment)?.clear();
|
|
||||||
controller.selectedImages.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
String _timeAgo(String dateString) {
|
|
||||||
// This logic remains unchanged
|
|
||||||
try {
|
|
||||||
final date = DateTime.parse(dateString + "Z").toLocal();
|
|
||||||
final difference = DateTime.now().difference(date);
|
|
||||||
|
|
||||||
if (difference.inDays > 8) return DateFormat('dd-MM-yyyy').format(date);
|
|
||||||
if (difference.inDays >= 1)
|
|
||||||
return '${difference.inDays} day${difference.inDays > 1 ? 's' : ''} ago';
|
|
||||||
if (difference.inHours >= 1)
|
|
||||||
return '${difference.inHours} hr${difference.inHours > 1 ? 's' : ''} ago';
|
|
||||||
if (difference.inMinutes >= 1)
|
|
||||||
return '${difference.inMinutes} min${difference.inMinutes > 1 ? 's' : ''} ago';
|
|
||||||
return 'just now';
|
|
||||||
} catch (e) {
|
|
||||||
debugPrint('Error parsing date for timeAgo: $e');
|
|
||||||
return dateString;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
// --- REFACTORING POINT ---
|
|
||||||
// The entire widget now returns a BaseBottomSheet, passing the content as its child.
|
|
||||||
// The GetBuilder provides reactive state (like isLoading) to the BaseBottomSheet.
|
|
||||||
return GetBuilder<ReportTaskController>(
|
|
||||||
tag: widget.taskData['taskId'] ?? '',
|
|
||||||
builder: (controller) {
|
|
||||||
return BaseBottomSheet(
|
|
||||||
title: "Task Details & Comments",
|
|
||||||
onCancel: () => Navigator.of(context).pop(),
|
|
||||||
onSubmit: _submitComment,
|
|
||||||
isSubmitting: controller.isLoading.value,
|
|
||||||
bottomContent: _buildCommentsSection(),
|
|
||||||
child: Form(
|
|
||||||
// moved to last
|
|
||||||
key: controller.basicValidator.formKey,
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
_buildHeaderActions(),
|
|
||||||
MySpacing.height(12),
|
|
||||||
_buildTaskDetails(),
|
|
||||||
_buildReportedImages(),
|
|
||||||
_buildCommentInput(),
|
|
||||||
_buildImagePicker(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- REFACTORING POINT ---
|
|
||||||
// The original _buildHeader is now split. The title is handled by BaseBottomSheet.
|
|
||||||
// This new widget contains the remaining actions from the header.
|
|
||||||
Widget _buildHeaderActions() {
|
|
||||||
return Align(
|
|
||||||
alignment: Alignment.centerRight,
|
|
||||||
child: InkWell(
|
|
||||||
onTap: () => _showCreateTaskBottomSheet(),
|
|
||||||
borderRadius: BorderRadius.circular(16),
|
|
||||||
child: Container(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.blueAccent.withOpacity(0.1),
|
|
||||||
borderRadius: BorderRadius.circular(16),
|
|
||||||
),
|
|
||||||
child: MyText.bodySmall(
|
|
||||||
"+ Create Task",
|
|
||||||
fontWeight: 600,
|
|
||||||
color: Colors.blueAccent,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildTaskDetails() {
|
|
||||||
return Column(
|
|
||||||
children: [
|
|
||||||
_buildDetailRow(
|
|
||||||
"Assigned By",
|
|
||||||
controller.basicValidator
|
|
||||||
.getController(_FormFieldKeys.assignedBy)
|
|
||||||
?.text,
|
|
||||||
icon: Icons.person_outline),
|
|
||||||
_buildDetailRow(
|
|
||||||
"Work Area",
|
|
||||||
controller.basicValidator
|
|
||||||
.getController(_FormFieldKeys.workArea)
|
|
||||||
?.text,
|
|
||||||
icon: Icons.place_outlined),
|
|
||||||
_buildDetailRow(
|
|
||||||
"Activity",
|
|
||||||
controller.basicValidator
|
|
||||||
.getController(_FormFieldKeys.activity)
|
|
||||||
?.text,
|
|
||||||
icon: Icons.assignment_outlined),
|
|
||||||
_buildDetailRow(
|
|
||||||
"Planned Work",
|
|
||||||
controller.basicValidator
|
|
||||||
.getController(_FormFieldKeys.plannedWork)
|
|
||||||
?.text,
|
|
||||||
icon: Icons.schedule_outlined),
|
|
||||||
_buildDetailRow(
|
|
||||||
"Completed Work",
|
|
||||||
controller.basicValidator
|
|
||||||
.getController(_FormFieldKeys.completedWork)
|
|
||||||
?.text,
|
|
||||||
icon: Icons.done_all_outlined),
|
|
||||||
_buildTeamMembers(),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildReportedImages() {
|
|
||||||
final imageUrls =
|
|
||||||
List<String>.from(widget.taskData['reportedPreSignedUrls'] ?? []);
|
|
||||||
if (imageUrls.isEmpty) return const SizedBox.shrink();
|
|
||||||
|
|
||||||
return Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(bottom: 8.0),
|
|
||||||
child: _buildSectionHeader("Reported Images", Icons.image_outlined),
|
|
||||||
),
|
|
||||||
// --- Refactoring Note ---
|
|
||||||
// Using the reusable _ImageHorizontalListView widget.
|
|
||||||
_ImageHorizontalListView(
|
|
||||||
imageSources: imageUrls,
|
|
||||||
onPreview: (index) => _showImageViewer(imageUrls, index),
|
|
||||||
),
|
|
||||||
MySpacing.height(16),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildCommentInput() {
|
|
||||||
return Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
_buildSectionHeader("Add Note", Icons.comment_outlined),
|
|
||||||
MySpacing.height(8),
|
|
||||||
TextFormField(
|
|
||||||
validator:
|
|
||||||
controller.basicValidator.getValidation(_FormFieldKeys.comment),
|
|
||||||
controller:
|
|
||||||
controller.basicValidator.getController(_FormFieldKeys.comment),
|
|
||||||
keyboardType: TextInputType.multiline,
|
|
||||||
maxLines: null, // Allows for multiline input
|
|
||||||
decoration: InputDecoration(
|
|
||||||
hintText: "eg: Work done successfully",
|
|
||||||
hintStyle: MyTextStyle.bodySmall(xMuted: true),
|
|
||||||
border: outlineInputBorder,
|
|
||||||
enabledBorder: outlineInputBorder,
|
|
||||||
focusedBorder: focusedInputBorder,
|
|
||||||
contentPadding: MySpacing.all(16),
|
|
||||||
isCollapsed: true,
|
|
||||||
floatingLabelBehavior: FloatingLabelBehavior.never,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
MySpacing.height(16),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildImagePicker() {
|
|
||||||
return Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
_buildSectionHeader("Attach Photos", Icons.camera_alt_outlined),
|
|
||||||
MySpacing.height(12),
|
|
||||||
Obx(() {
|
|
||||||
final images = controller.selectedImages;
|
|
||||||
return Column(
|
|
||||||
children: [
|
|
||||||
// --- Refactoring Note ---
|
|
||||||
// Using the reusable _ImageHorizontalListView for picked images.
|
|
||||||
_ImageHorizontalListView(
|
|
||||||
imageSources: images.toList(),
|
|
||||||
onPreview: (index) => _showImageViewer(images.toList(), index),
|
|
||||||
onRemove: (index) => controller.removeImageAt(index),
|
|
||||||
emptyStatePlaceholder: Container(
|
|
||||||
height: 70,
|
|
||||||
width: double.infinity,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
border: Border.all(color: Colors.grey.shade300, width: 1.5),
|
|
||||||
color: Colors.grey.shade100,
|
|
||||||
),
|
|
||||||
child: Center(
|
|
||||||
child: Icon(Icons.photo_library_outlined,
|
|
||||||
size: 36, color: Colors.grey.shade400),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
MySpacing.height(16),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
_buildPickerButton(
|
|
||||||
onTap: () => controller.pickImages(fromCamera: true),
|
|
||||||
icon: Icons.camera_alt,
|
|
||||||
label: 'Capture',
|
|
||||||
),
|
|
||||||
MySpacing.width(12),
|
|
||||||
_buildPickerButton(
|
|
||||||
onTap: () => controller.pickImages(fromCamera: false),
|
|
||||||
icon: Icons.upload_file,
|
|
||||||
label: 'Upload',
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildCommentsSection() {
|
|
||||||
if (_sortedComments.isEmpty) return const SizedBox.shrink();
|
|
||||||
|
|
||||||
return Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
MySpacing.height(24),
|
|
||||||
_buildSectionHeader("Comments", Icons.chat_bubble_outline),
|
|
||||||
MySpacing.height(12),
|
|
||||||
// --- Refactoring Note ---
|
|
||||||
// Using a ListView instead of a fixed-height SizedBox for better responsiveness.
|
|
||||||
// It's constrained by the parent SingleChildScrollView.
|
|
||||||
ListView.builder(
|
|
||||||
shrinkWrap:
|
|
||||||
true, // Important for ListView inside SingleChildScrollView
|
|
||||||
physics:
|
|
||||||
const NeverScrollableScrollPhysics(), // Parent handles scrolling
|
|
||||||
itemCount: _sortedComments.length,
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
final comment = _sortedComments[index];
|
|
||||||
// --- Refactoring Note ---
|
|
||||||
// Extracted the comment item into its own widget for clarity.
|
|
||||||
return _CommentCard(
|
|
||||||
comment: comment,
|
|
||||||
timeAgo: _timeAgo(comment['date'] ?? ''),
|
|
||||||
onPreviewImage: (imageUrls, idx) =>
|
|
||||||
_showImageViewer(imageUrls, idx),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Helper and Builder methods ---
|
|
||||||
|
|
||||||
Widget _buildDetailRow(String label, String? value,
|
|
||||||
{required IconData icon}) {
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.only(bottom: 16),
|
|
||||||
child: Row(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(right: 8.0, top: 2),
|
|
||||||
child: Icon(icon, size: 18, color: Colors.grey[700]),
|
|
||||||
),
|
|
||||||
MyText.titleSmall("$label:", fontWeight: 600),
|
|
||||||
MySpacing.width(12),
|
|
||||||
Expanded(
|
|
||||||
child: MyText.bodyMedium(
|
|
||||||
value != null && value.isNotEmpty ? value : "-",
|
|
||||||
color: Colors.black87,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildSectionHeader(String title, IconData icon) {
|
|
||||||
return Row(
|
|
||||||
children: [
|
|
||||||
Icon(icon, size: 18, color: Colors.grey[700]),
|
|
||||||
MySpacing.width(8),
|
|
||||||
MyText.titleSmall(title, fontWeight: 600),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildTeamMembers() {
|
|
||||||
final teamMembersText = controller.basicValidator
|
|
||||||
.getController(_FormFieldKeys.teamMembers)
|
|
||||||
?.text ??
|
|
||||||
'';
|
|
||||||
final members = teamMembersText
|
|
||||||
.split(',')
|
|
||||||
.map((e) => e.trim())
|
|
||||||
.where((e) => e.isNotEmpty)
|
|
||||||
.toList();
|
|
||||||
if (members.isEmpty) return const SizedBox.shrink();
|
|
||||||
|
|
||||||
const double avatarSize = 32.0;
|
|
||||||
const double avatarOverlap = 22.0;
|
|
||||||
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.only(bottom: 16.0),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Icon(Icons.group_outlined, size: 18, color: Colors.grey[700]),
|
|
||||||
MySpacing.width(8),
|
|
||||||
MyText.titleSmall("Team:", fontWeight: 600),
|
|
||||||
MySpacing.width(12),
|
|
||||||
GestureDetector(
|
|
||||||
onTap: () => TeamBottomSheet.show(
|
|
||||||
context: context,
|
|
||||||
teamMembers: members.map((name) => _Member(name)).toList()),
|
|
||||||
child: SizedBox(
|
|
||||||
height: avatarSize,
|
|
||||||
// Calculate width based on number of avatars shown
|
|
||||||
width: (math.min(members.length, 3) * avatarOverlap) +
|
|
||||||
(avatarSize - avatarOverlap),
|
|
||||||
child: Stack(
|
|
||||||
children: [
|
|
||||||
...List.generate(math.min(members.length, 3), (i) {
|
|
||||||
return Positioned(
|
|
||||||
left: i * avatarOverlap,
|
|
||||||
child: Tooltip(
|
|
||||||
message: members[i],
|
|
||||||
child: Avatar(
|
|
||||||
firstName: members[i],
|
|
||||||
lastName: '',
|
|
||||||
size: avatarSize),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
if (members.length > 3)
|
|
||||||
Positioned(
|
|
||||||
left: 3 * avatarOverlap,
|
|
||||||
child: CircleAvatar(
|
|
||||||
radius: avatarSize / 2,
|
|
||||||
backgroundColor: Colors.grey.shade300,
|
|
||||||
child: MyText.bodySmall('+${members.length - 3}',
|
|
||||||
fontWeight: 600),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildPickerButton(
|
|
||||||
{required VoidCallback onTap,
|
|
||||||
required IconData icon,
|
|
||||||
required String label}) {
|
|
||||||
return Expanded(
|
|
||||||
child: MyButton.outlined(
|
|
||||||
onPressed: onTap,
|
|
||||||
padding: MySpacing.xy(12, 10),
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
Icon(icon, size: 18, color: Colors.blueAccent),
|
|
||||||
MySpacing.width(8),
|
|
||||||
MyText.bodySmall(label, color: Colors.blueAccent, fontWeight: 600),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Action Handlers ---
|
|
||||||
|
|
||||||
void _showCreateTaskBottomSheet() {
|
|
||||||
showCreateTaskBottomSheet(
|
|
||||||
workArea: widget.taskData['location'] ?? '',
|
|
||||||
activity: widget.taskData['activity'] ?? '',
|
|
||||||
completedWork: widget.taskData['completedWork'] ?? '',
|
|
||||||
unit: widget.taskData['unit'] ?? '',
|
|
||||||
onCategoryChanged: (category) =>
|
|
||||||
debugPrint("Category changed to: $category"),
|
|
||||||
parentTaskId: widget.taskDataId,
|
|
||||||
plannedTask: int.tryParse(widget.taskData['plannedWork'] ?? '0') ?? 0,
|
|
||||||
activityId: widget.activityId,
|
|
||||||
workAreaId: widget.workAreaId,
|
|
||||||
onSubmit: () => Navigator.of(context).pop(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _showImageViewer(List<dynamic> sources, int initialIndex) {
|
|
||||||
showDialog(
|
|
||||||
context: context,
|
|
||||||
barrierColor: Colors.black87,
|
|
||||||
builder: (_) => ImageViewerDialog(
|
|
||||||
imageSources: sources,
|
|
||||||
initialIndex: initialIndex,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _submitComment() async {
|
|
||||||
if (controller.basicValidator.validateForm()) {
|
|
||||||
await controller.commentTask(
|
|
||||||
projectId: controller.basicValidator
|
|
||||||
.getController(_FormFieldKeys.taskId)
|
|
||||||
?.text ??
|
|
||||||
'',
|
|
||||||
comment: controller.basicValidator
|
|
||||||
.getController(_FormFieldKeys.comment)
|
|
||||||
?.text ??
|
|
||||||
'',
|
|
||||||
images: controller.selectedImages,
|
|
||||||
);
|
|
||||||
// Callback to the parent widget to refresh data if needed
|
|
||||||
widget.onCommentSuccess?.call();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Refactoring Note ---
|
|
||||||
// A reusable widget for displaying a horizontal list of images.
|
|
||||||
// It can handle both network URLs (String) and local files (File).
|
|
||||||
class _ImageHorizontalListView extends StatelessWidget {
|
|
||||||
final List<dynamic> imageSources; // Can be List<String> or List<File>
|
|
||||||
final Function(int) onPreview;
|
|
||||||
final Function(int)? onRemove;
|
|
||||||
final Widget? emptyStatePlaceholder;
|
|
||||||
|
|
||||||
const _ImageHorizontalListView({
|
|
||||||
required this.imageSources,
|
|
||||||
required this.onPreview,
|
|
||||||
this.onRemove,
|
|
||||||
this.emptyStatePlaceholder,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
if (imageSources.isEmpty) {
|
|
||||||
return emptyStatePlaceholder ?? const SizedBox.shrink();
|
|
||||||
}
|
|
||||||
|
|
||||||
return SizedBox(
|
|
||||||
height: 70,
|
|
||||||
child: ListView.separated(
|
|
||||||
scrollDirection: Axis.horizontal,
|
|
||||||
itemCount: imageSources.length,
|
|
||||||
separatorBuilder: (_, __) => const SizedBox(width: 12),
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
final source = imageSources[index];
|
|
||||||
return GestureDetector(
|
|
||||||
onTap: () => onPreview(index),
|
|
||||||
child: Stack(
|
|
||||||
clipBehavior: Clip.none,
|
|
||||||
children: [
|
|
||||||
ClipRRect(
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
child: source is File
|
|
||||||
? Image.file(source,
|
|
||||||
width: 70, height: 70, fit: BoxFit.cover)
|
|
||||||
: Image.network(
|
|
||||||
source as String,
|
|
||||||
width: 70,
|
|
||||||
height: 70,
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
errorBuilder: (context, error, stackTrace) =>
|
|
||||||
Container(
|
|
||||||
width: 70,
|
|
||||||
height: 70,
|
|
||||||
color: Colors.grey.shade200,
|
|
||||||
child: Icon(Icons.broken_image,
|
|
||||||
color: Colors.grey[600]),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (onRemove != null)
|
|
||||||
Positioned(
|
|
||||||
top: -6,
|
|
||||||
right: -6,
|
|
||||||
child: GestureDetector(
|
|
||||||
onTap: () => onRemove!(index),
|
|
||||||
child: Container(
|
|
||||||
padding: const EdgeInsets.all(2),
|
|
||||||
decoration: const BoxDecoration(
|
|
||||||
color: Colors.red, shape: BoxShape.circle),
|
|
||||||
child: const Icon(Icons.close,
|
|
||||||
size: 16, color: Colors.white),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Refactoring Note ---
|
|
||||||
// A dedicated widget for a single comment card. This cleans up the main
|
|
||||||
// widget's build method and makes the comment layout easier to manage.
|
|
||||||
class _CommentCard extends StatelessWidget {
|
|
||||||
final Map<String, dynamic> comment;
|
|
||||||
final String timeAgo;
|
|
||||||
final Function(List<String> imageUrls, int index) onPreviewImage;
|
|
||||||
|
|
||||||
const _CommentCard({
|
|
||||||
required this.comment,
|
|
||||||
required this.timeAgo,
|
|
||||||
required this.onPreviewImage,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final commentedBy = comment['commentedBy'] ?? 'Unknown';
|
|
||||||
final commentText = comment['text'] ?? '-';
|
|
||||||
final imageUrls = List<String>.from(comment['preSignedUrls'] ?? []);
|
|
||||||
|
|
||||||
return Container(
|
|
||||||
margin: const EdgeInsets.symmetric(vertical: 6),
|
|
||||||
padding: const EdgeInsets.all(12),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.grey.shade100,
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
border: Border.all(color: Colors.grey.shade200)),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Avatar(
|
|
||||||
firstName: commentedBy.split(' ').first,
|
|
||||||
lastName: commentedBy.split(' ').length > 1
|
|
||||||
? commentedBy.split(' ').last
|
|
||||||
: '',
|
|
||||||
size: 32,
|
|
||||||
),
|
|
||||||
MySpacing.width(12),
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
MyText.bodyMedium(commentedBy,
|
|
||||||
fontWeight: 700, color: Colors.black87),
|
|
||||||
MyText.bodySmall(timeAgo,
|
|
||||||
color: Colors.black54, fontSize: 12),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
MySpacing.height(12),
|
|
||||||
MyText.bodyMedium(commentText, color: Colors.black87),
|
|
||||||
if (imageUrls.isNotEmpty) ...[
|
|
||||||
MySpacing.height(12),
|
|
||||||
_ImageHorizontalListView(
|
|
||||||
imageSources: imageUrls,
|
|
||||||
onPreview: (index) => onPreviewImage(imageUrls, index),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,213 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:get/get.dart';
|
|
||||||
import 'package:marco/controller/task_planning/add_task_controller.dart';
|
|
||||||
import 'package:marco/helpers/widgets/my_text.dart';
|
|
||||||
|
|
||||||
import 'package:marco/helpers/widgets/my_snackbar.dart';
|
|
||||||
import 'package:marco/helpers/utils/base_bottom_sheet.dart';
|
|
||||||
|
|
||||||
void showCreateTaskBottomSheet({
|
|
||||||
required String workArea,
|
|
||||||
required String activity,
|
|
||||||
required String completedWork,
|
|
||||||
required String unit,
|
|
||||||
required Function(String) onCategoryChanged,
|
|
||||||
required String parentTaskId,
|
|
||||||
required int plannedTask,
|
|
||||||
required String activityId,
|
|
||||||
required String workAreaId,
|
|
||||||
required VoidCallback onSubmit,
|
|
||||||
}) {
|
|
||||||
final controller = Get.put(AddTaskController());
|
|
||||||
final TextEditingController plannedTaskController =
|
|
||||||
TextEditingController(text: plannedTask.toString());
|
|
||||||
final TextEditingController descriptionController = TextEditingController();
|
|
||||||
|
|
||||||
Get.bottomSheet(
|
|
||||||
StatefulBuilder(
|
|
||||||
builder: (context, setState) {
|
|
||||||
return BaseBottomSheet(
|
|
||||||
title: "Create Task",
|
|
||||||
onCancel: () => Get.back(),
|
|
||||||
onSubmit: () async {
|
|
||||||
final plannedValue =
|
|
||||||
int.tryParse(plannedTaskController.text.trim()) ?? 0;
|
|
||||||
final comment = descriptionController.text.trim();
|
|
||||||
final selectedCategoryId = controller.selectedCategoryId.value;
|
|
||||||
|
|
||||||
if (selectedCategoryId == null) {
|
|
||||||
showAppSnackbar(
|
|
||||||
title: "error",
|
|
||||||
message: "Please select a work category!",
|
|
||||||
type: SnackbarType.error,
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final success = await controller.createTask(
|
|
||||||
parentTaskId: parentTaskId,
|
|
||||||
plannedTask: plannedValue,
|
|
||||||
comment: comment,
|
|
||||||
workAreaId: workAreaId,
|
|
||||||
activityId: activityId,
|
|
||||||
categoryId: selectedCategoryId,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (success) {
|
|
||||||
Get.back();
|
|
||||||
Future.delayed(const Duration(milliseconds: 300), () {
|
|
||||||
onSubmit();
|
|
||||||
showAppSnackbar(
|
|
||||||
title: "Success",
|
|
||||||
message: "Task created successfully!",
|
|
||||||
type: SnackbarType.success,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
submitText: "Submit",
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
_infoCardSection([
|
|
||||||
_infoRowWithIcon(
|
|
||||||
Icons.workspaces, "Selected Work Area", workArea),
|
|
||||||
_infoRowWithIcon(Icons.list_alt, "Selected Activity", activity),
|
|
||||||
_infoRowWithIcon(Icons.check_circle_outline, "Completed Work",
|
|
||||||
completedWork),
|
|
||||||
]),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
_sectionTitle(Icons.edit_calendar, "Planned Work"),
|
|
||||||
const SizedBox(height: 6),
|
|
||||||
_customTextField(
|
|
||||||
controller: plannedTaskController,
|
|
||||||
hint: "Enter planned work",
|
|
||||||
keyboardType: TextInputType.number,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
_sectionTitle(Icons.description_outlined, "Comment"),
|
|
||||||
const SizedBox(height: 6),
|
|
||||||
_customTextField(
|
|
||||||
controller: descriptionController,
|
|
||||||
hint: "Enter task description",
|
|
||||||
maxLines: 3,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
_sectionTitle(Icons.category_outlined, "Selected Work Category"),
|
|
||||||
const SizedBox(height: 6),
|
|
||||||
Obx(() {
|
|
||||||
final categoryMap = controller.categoryIdNameMap;
|
|
||||||
final String selectedName =
|
|
||||||
controller.selectedCategoryId.value != null
|
|
||||||
? (categoryMap[controller.selectedCategoryId.value!] ??
|
|
||||||
'Select Category')
|
|
||||||
: 'Select Category';
|
|
||||||
|
|
||||||
return Container(
|
|
||||||
width: double.infinity,
|
|
||||||
padding:
|
|
||||||
const EdgeInsets.symmetric(horizontal: 12, vertical: 14),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.grey.shade100,
|
|
||||||
border: Border.all(color: Colors.grey.shade300),
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
),
|
|
||||||
child: PopupMenuButton<String>(
|
|
||||||
padding: EdgeInsets.zero,
|
|
||||||
onSelected: (val) {
|
|
||||||
controller.selectCategory(val);
|
|
||||||
onCategoryChanged(val);
|
|
||||||
},
|
|
||||||
itemBuilder: (context) => categoryMap.entries
|
|
||||||
.map((entry) => PopupMenuItem<String>(
|
|
||||||
value: entry.key,
|
|
||||||
child: Text(entry.value),
|
|
||||||
))
|
|
||||||
.toList(),
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
selectedName,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 14, color: Colors.black87),
|
|
||||||
),
|
|
||||||
const Icon(Icons.arrow_drop_down),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
isScrollControlled: true,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _sectionTitle(IconData icon, String title) {
|
|
||||||
return Row(
|
|
||||||
children: [
|
|
||||||
Icon(icon, color: Colors.grey[700], size: 18),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
MyText.bodyMedium(title, fontWeight: 600),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _customTextField({
|
|
||||||
required TextEditingController controller,
|
|
||||||
required String hint,
|
|
||||||
int maxLines = 1,
|
|
||||||
TextInputType keyboardType = TextInputType.text,
|
|
||||||
}) {
|
|
||||||
return TextField(
|
|
||||||
controller: controller,
|
|
||||||
maxLines: maxLines,
|
|
||||||
keyboardType: keyboardType,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
hintText: hint,
|
|
||||||
filled: true,
|
|
||||||
fillColor: Colors.grey.shade100,
|
|
||||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
|
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _infoCardSection(List<Widget> children) {
|
|
||||||
return Container(
|
|
||||||
padding: const EdgeInsets.all(12),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.grey.shade100,
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
border: Border.all(color: Colors.grey.shade300),
|
|
||||||
),
|
|
||||||
child: Column(children: children),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _infoRowWithIcon(IconData icon, String title, String value) {
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 6),
|
|
||||||
child: Row(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Icon(icon, color: Colors.grey[700], size: 18),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
MyText.bodyMedium(title, fontWeight: 600),
|
|
||||||
const SizedBox(height: 2),
|
|
||||||
MyText.bodySmall(value, color: Colors.grey[800]),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
@ -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),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,222 +0,0 @@
|
|||||||
class TaskModel {
|
|
||||||
final DateTime assignmentDate;
|
|
||||||
final DateTime? reportedDate;
|
|
||||||
final String id;
|
|
||||||
final WorkItem? workItem;
|
|
||||||
final String workItemId;
|
|
||||||
final double plannedTask;
|
|
||||||
final double completedTask;
|
|
||||||
final AssignedBy assignedBy;
|
|
||||||
final AssignedBy? approvedBy;
|
|
||||||
final List<TeamMember> teamMembers;
|
|
||||||
final List<Comment> comments;
|
|
||||||
final List<String> reportedPreSignedUrls;
|
|
||||||
|
|
||||||
TaskModel({
|
|
||||||
required this.assignmentDate,
|
|
||||||
this.reportedDate,
|
|
||||||
required this.id,
|
|
||||||
required this.workItem,
|
|
||||||
required this.workItemId,
|
|
||||||
required this.plannedTask,
|
|
||||||
required this.completedTask,
|
|
||||||
required this.assignedBy,
|
|
||||||
this.approvedBy,
|
|
||||||
required this.teamMembers,
|
|
||||||
required this.comments,
|
|
||||||
required this.reportedPreSignedUrls,
|
|
||||||
});
|
|
||||||
|
|
||||||
factory TaskModel.fromJson(Map<String, dynamic> json) {
|
|
||||||
return TaskModel(
|
|
||||||
assignmentDate: DateTime.parse(json['assignmentDate']),
|
|
||||||
reportedDate: json['reportedDate'] != null
|
|
||||||
? DateTime.tryParse(json['reportedDate'])
|
|
||||||
: null,
|
|
||||||
id: json['id'],
|
|
||||||
workItem:
|
|
||||||
json['workItem'] != null ? WorkItem.fromJson(json['workItem']) : null,
|
|
||||||
workItemId: json['workItemId'],
|
|
||||||
plannedTask: (json['plannedTask'] as num).toDouble(),
|
|
||||||
completedTask: (json['completedTask'] as num).toDouble(),
|
|
||||||
assignedBy: AssignedBy.fromJson(json['assignedBy']),
|
|
||||||
approvedBy: json['approvedBy'] != null
|
|
||||||
? AssignedBy.fromJson(json['approvedBy'])
|
|
||||||
: null,
|
|
||||||
teamMembers: (json['teamMembers'] as List)
|
|
||||||
.map((e) => TeamMember.fromJson(e))
|
|
||||||
.toList(),
|
|
||||||
comments:
|
|
||||||
(json['comments'] as List).map((e) => Comment.fromJson(e)).toList(),
|
|
||||||
reportedPreSignedUrls: (json['reportedPreSignedUrls'] as List<dynamic>?)
|
|
||||||
?.map((e) => e.toString())
|
|
||||||
.toList() ??
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class WorkItem {
|
|
||||||
final String? id;
|
|
||||||
final ActivityMaster? activityMaster;
|
|
||||||
final WorkArea? workArea;
|
|
||||||
final double? plannedWork;
|
|
||||||
final double? completedWork;
|
|
||||||
final List<String> preSignedUrls;
|
|
||||||
|
|
||||||
WorkItem({
|
|
||||||
this.id,
|
|
||||||
this.activityMaster,
|
|
||||||
this.workArea,
|
|
||||||
this.plannedWork,
|
|
||||||
this.completedWork,
|
|
||||||
this.preSignedUrls = const [],
|
|
||||||
});
|
|
||||||
|
|
||||||
factory WorkItem.fromJson(Map<String, dynamic> json) {
|
|
||||||
return WorkItem(
|
|
||||||
id: json['id']?.toString(),
|
|
||||||
activityMaster: json['activityMaster'] != null
|
|
||||||
? ActivityMaster.fromJson(json['activityMaster'])
|
|
||||||
: null,
|
|
||||||
workArea:
|
|
||||||
json['workArea'] != null ? WorkArea.fromJson(json['workArea']) : null,
|
|
||||||
plannedWork: (json['plannedWork'] as num?)?.toDouble(),
|
|
||||||
completedWork: (json['completedWork'] as num?)?.toDouble(),
|
|
||||||
preSignedUrls: (json['preSignedUrls'] as List<dynamic>?)
|
|
||||||
?.map((e) => e.toString())
|
|
||||||
.toList() ??
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ActivityMaster {
|
|
||||||
final String? id; // ✅ Added
|
|
||||||
final String activityName;
|
|
||||||
|
|
||||||
ActivityMaster({
|
|
||||||
this.id,
|
|
||||||
required this.activityName,
|
|
||||||
});
|
|
||||||
|
|
||||||
factory ActivityMaster.fromJson(Map<String, dynamic> json) {
|
|
||||||
return ActivityMaster(
|
|
||||||
id: json['id']?.toString(),
|
|
||||||
activityName: json['activityName'] ?? '',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class WorkArea {
|
|
||||||
final String? id; // ✅ Added
|
|
||||||
final String areaName;
|
|
||||||
final Floor? floor;
|
|
||||||
|
|
||||||
WorkArea({
|
|
||||||
this.id,
|
|
||||||
required this.areaName,
|
|
||||||
this.floor,
|
|
||||||
});
|
|
||||||
|
|
||||||
factory WorkArea.fromJson(Map<String, dynamic> json) {
|
|
||||||
return WorkArea(
|
|
||||||
id: json['id']?.toString(),
|
|
||||||
areaName: json['areaName'] ?? '',
|
|
||||||
floor: json['floor'] != null ? Floor.fromJson(json['floor']) : null,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class Floor {
|
|
||||||
final String floorName;
|
|
||||||
final Building? building;
|
|
||||||
|
|
||||||
Floor({required this.floorName, this.building});
|
|
||||||
|
|
||||||
factory Floor.fromJson(Map<String, dynamic> json) {
|
|
||||||
return Floor(
|
|
||||||
floorName: json['floorName'] ?? '',
|
|
||||||
building:
|
|
||||||
json['building'] != null ? Building.fromJson(json['building']) : null,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class Building {
|
|
||||||
final String name;
|
|
||||||
|
|
||||||
Building({required this.name});
|
|
||||||
|
|
||||||
factory Building.fromJson(Map<String, dynamic> json) {
|
|
||||||
return Building(name: json['name'] ?? '');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class AssignedBy {
|
|
||||||
final String id;
|
|
||||||
final String firstName;
|
|
||||||
final String? lastName;
|
|
||||||
|
|
||||||
AssignedBy({
|
|
||||||
required this.id,
|
|
||||||
required this.firstName,
|
|
||||||
this.lastName,
|
|
||||||
});
|
|
||||||
|
|
||||||
factory AssignedBy.fromJson(Map<String, dynamic> json) {
|
|
||||||
return AssignedBy(
|
|
||||||
id: json['id']?.toString() ?? '',
|
|
||||||
firstName: json['firstName'] ?? '',
|
|
||||||
lastName: json['lastName'],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class TeamMember {
|
|
||||||
final String id;
|
|
||||||
final String firstName;
|
|
||||||
final String? lastName;
|
|
||||||
|
|
||||||
TeamMember({
|
|
||||||
required this.id,
|
|
||||||
required this.firstName,
|
|
||||||
this.lastName,
|
|
||||||
});
|
|
||||||
|
|
||||||
factory TeamMember.fromJson(Map<String, dynamic> json) {
|
|
||||||
return TeamMember(
|
|
||||||
id: json['id']?.toString() ?? '',
|
|
||||||
firstName: json['firstName']?.toString() ?? '',
|
|
||||||
lastName: json['lastName']?.toString(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class Comment {
|
|
||||||
final String comment;
|
|
||||||
final TeamMember commentedBy;
|
|
||||||
final DateTime timestamp;
|
|
||||||
final List<String> preSignedUrls;
|
|
||||||
|
|
||||||
Comment({
|
|
||||||
required this.comment,
|
|
||||||
required this.commentedBy,
|
|
||||||
required this.timestamp,
|
|
||||||
required this.preSignedUrls,
|
|
||||||
});
|
|
||||||
|
|
||||||
factory Comment.fromJson(Map<String, dynamic> json) {
|
|
||||||
return Comment(
|
|
||||||
comment: json['comment']?.toString() ?? '',
|
|
||||||
commentedBy: json['employee'] != null
|
|
||||||
? TeamMember.fromJson(json['employee'])
|
|
||||||
: TeamMember(id: '', firstName: '', lastName: null),
|
|
||||||
timestamp: DateTime.parse(json['commentDate'] ?? ''),
|
|
||||||
preSignedUrls: (json['preSignedUrls'] as List<dynamic>?)
|
|
||||||
?.map((e) => e.toString())
|
|
||||||
.toList() ??
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,144 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:marco/controller/permission_controller.dart';
|
|
||||||
import 'package:marco/controller/task_Planning/daily_task_Planning_controller.dart';
|
|
||||||
import 'package:marco/helpers/widgets/my_text.dart';
|
|
||||||
|
|
||||||
class DailyTaskPlanningFilter extends StatelessWidget {
|
|
||||||
final DailyTaskPlanningController controller;
|
|
||||||
final PermissionController permissionController;
|
|
||||||
|
|
||||||
const DailyTaskPlanningFilter({
|
|
||||||
super.key,
|
|
||||||
required this.controller,
|
|
||||||
required this.permissionController,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
String? tempSelectedProjectId = '654563563645';
|
|
||||||
bool showProjectList = false;
|
|
||||||
|
|
||||||
final accessibleProjects = controller.projects
|
|
||||||
.where((project) =>
|
|
||||||
permissionController.isUserAssignedToProject(project.id.toString()))
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
return StatefulBuilder(builder: (context, setState) {
|
|
||||||
List<Widget> filterWidgets;
|
|
||||||
|
|
||||||
if (showProjectList) {
|
|
||||||
filterWidgets = accessibleProjects.isEmpty
|
|
||||||
? [
|
|
||||||
Padding(
|
|
||||||
padding: EdgeInsets.all(12.0),
|
|
||||||
child: Center(
|
|
||||||
child: MyText.titleSmall(
|
|
||||||
'No Projects Assigned',
|
|
||||||
fontWeight: 600,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
: accessibleProjects.map((project) {
|
|
||||||
final isSelected =
|
|
||||||
tempSelectedProjectId == project.id.toString();
|
|
||||||
return ListTile(
|
|
||||||
dense: true,
|
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
|
|
||||||
title: MyText.titleSmall(project.name),
|
|
||||||
trailing: isSelected ? const Icon(Icons.check) : null,
|
|
||||||
onTap: () {
|
|
||||||
setState(() {
|
|
||||||
tempSelectedProjectId = project.id.toString();
|
|
||||||
showProjectList = false;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}).toList();
|
|
||||||
} else {
|
|
||||||
final selectedProject = accessibleProjects.isNotEmpty
|
|
||||||
? accessibleProjects.firstWhere(
|
|
||||||
(p) => p.id.toString() == tempSelectedProjectId,
|
|
||||||
orElse: () => accessibleProjects[0],
|
|
||||||
)
|
|
||||||
: null;
|
|
||||||
|
|
||||||
final selectedProjectName = selectedProject?.name ?? "Select Project";
|
|
||||||
|
|
||||||
filterWidgets = [
|
|
||||||
Padding(
|
|
||||||
padding: EdgeInsets.fromLTRB(16, 12, 16, 4),
|
|
||||||
child: Align(
|
|
||||||
alignment: Alignment.centerLeft,
|
|
||||||
child: MyText.titleSmall(
|
|
||||||
'Select Project',
|
|
||||||
fontWeight: 600,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
ListTile(
|
|
||||||
dense: true,
|
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
|
|
||||||
title: MyText.titleSmall(selectedProjectName),
|
|
||||||
trailing: const Icon(Icons.arrow_drop_down),
|
|
||||||
onTap: () => setState(() => showProjectList = true),
|
|
||||||
),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
return Padding(
|
|
||||||
padding: EdgeInsets.only(
|
|
||||||
bottom: MediaQuery.of(context).viewInsets.bottom,
|
|
||||||
),
|
|
||||||
child: SingleChildScrollView(
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(top: 12, bottom: 8),
|
|
||||||
child: Center(
|
|
||||||
child: Container(
|
|
||||||
width: 40,
|
|
||||||
height: 4,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.grey[400],
|
|
||||||
borderRadius: BorderRadius.circular(4),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
...filterWidgets,
|
|
||||||
const Divider(),
|
|
||||||
Padding(
|
|
||||||
padding:
|
|
||||||
const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
|
||||||
child: SizedBox(
|
|
||||||
width: double.infinity,
|
|
||||||
child: ElevatedButton(
|
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
backgroundColor: Color.fromARGB(255, 95, 132, 255),
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: MyText.titleSmall(
|
|
||||||
'Apply Filter',
|
|
||||||
fontWeight: 600,
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
onPressed: () {
|
|
||||||
Navigator.pop(context, {
|
|
||||||
'projectId': tempSelectedProjectId,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,246 +0,0 @@
|
|||||||
class TaskPlanningDetailsModel {
|
|
||||||
final List<Building> buildings;
|
|
||||||
final String id;
|
|
||||||
final String name;
|
|
||||||
final String projectAddress;
|
|
||||||
final String contactPerson;
|
|
||||||
final DateTime startDate;
|
|
||||||
final DateTime endDate;
|
|
||||||
final String projectStatusId;
|
|
||||||
|
|
||||||
TaskPlanningDetailsModel({
|
|
||||||
required this.buildings,
|
|
||||||
required this.id,
|
|
||||||
required this.name,
|
|
||||||
required this.projectAddress,
|
|
||||||
required this.contactPerson,
|
|
||||||
required this.startDate,
|
|
||||||
required this.endDate,
|
|
||||||
required this.projectStatusId,
|
|
||||||
});
|
|
||||||
|
|
||||||
factory TaskPlanningDetailsModel.fromJson(Map<String, dynamic> json) {
|
|
||||||
return TaskPlanningDetailsModel(
|
|
||||||
buildings: (json['buildings'] as List<dynamic>?)
|
|
||||||
?.map((b) => Building.fromJson(b))
|
|
||||||
.toList() ??
|
|
||||||
[],
|
|
||||||
id: json['id'],
|
|
||||||
name: json['name'],
|
|
||||||
projectAddress: json['projectAddress'],
|
|
||||||
contactPerson: json['contactPerson'],
|
|
||||||
startDate: DateTime.parse(json['startDate']),
|
|
||||||
endDate: DateTime.parse(json['endDate']),
|
|
||||||
projectStatusId: json['projectStatusId'],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class Building {
|
|
||||||
final String id;
|
|
||||||
final String name;
|
|
||||||
final String description;
|
|
||||||
final List<Floor> floors;
|
|
||||||
|
|
||||||
Building({
|
|
||||||
required this.id,
|
|
||||||
required this.name,
|
|
||||||
required this.description,
|
|
||||||
required this.floors,
|
|
||||||
});
|
|
||||||
|
|
||||||
factory Building.fromJson(Map<String, dynamic> json) {
|
|
||||||
return Building(
|
|
||||||
id: json['id'],
|
|
||||||
name: json['name'],
|
|
||||||
description: json['description'],
|
|
||||||
floors: (json['floors'] as List).map((f) => Floor.fromJson(f)).toList(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class Floor {
|
|
||||||
final String id;
|
|
||||||
final String floorName;
|
|
||||||
final List<WorkArea> workAreas;
|
|
||||||
|
|
||||||
Floor({
|
|
||||||
required this.id,
|
|
||||||
required this.floorName,
|
|
||||||
required this.workAreas,
|
|
||||||
});
|
|
||||||
|
|
||||||
factory Floor.fromJson(Map<String, dynamic> json) {
|
|
||||||
return Floor(
|
|
||||||
id: json['id'],
|
|
||||||
floorName: json['floorName'],
|
|
||||||
workAreas:
|
|
||||||
(json['workAreas'] as List).map((w) => WorkArea.fromJson(w)).toList(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class WorkArea {
|
|
||||||
final String id;
|
|
||||||
final String areaName;
|
|
||||||
final List<WorkItemWrapper> workItems;
|
|
||||||
|
|
||||||
WorkArea({
|
|
||||||
required this.id,
|
|
||||||
required this.areaName,
|
|
||||||
required this.workItems,
|
|
||||||
});
|
|
||||||
|
|
||||||
factory WorkArea.fromJson(Map<String, dynamic> json) {
|
|
||||||
return WorkArea(
|
|
||||||
id: json['id'],
|
|
||||||
areaName: json['areaName'],
|
|
||||||
workItems: (json['workItems'] as List)
|
|
||||||
.map((w) => WorkItemWrapper.fromJson(w))
|
|
||||||
.toList(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class WorkItemWrapper {
|
|
||||||
final String workItemId;
|
|
||||||
final WorkItem workItem;
|
|
||||||
|
|
||||||
WorkItemWrapper({
|
|
||||||
required this.workItemId,
|
|
||||||
required this.workItem,
|
|
||||||
});
|
|
||||||
|
|
||||||
factory WorkItemWrapper.fromJson(Map<String, dynamic> json) {
|
|
||||||
return WorkItemWrapper(
|
|
||||||
workItemId: json['workItemId'],
|
|
||||||
workItem: WorkItem.fromJson(json['workItem']),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class WorkItem {
|
|
||||||
final String? id;
|
|
||||||
final String? activityId;
|
|
||||||
final String? workCategoryId;
|
|
||||||
final String? workAreaId;
|
|
||||||
final WorkAreaBasic? workArea;
|
|
||||||
final ActivityMaster? activityMaster;
|
|
||||||
final WorkCategoryMaster? workCategoryMaster;
|
|
||||||
final double? plannedWork;
|
|
||||||
final double? completedWork;
|
|
||||||
final String? description;
|
|
||||||
final double? todaysAssigned;
|
|
||||||
final DateTime? taskDate;
|
|
||||||
final String? tenantId;
|
|
||||||
final Tenant? tenant;
|
|
||||||
|
|
||||||
WorkItem({
|
|
||||||
this.id,
|
|
||||||
this.activityId,
|
|
||||||
this.workCategoryId,
|
|
||||||
this.workAreaId,
|
|
||||||
this.workArea,
|
|
||||||
this.activityMaster,
|
|
||||||
this.workCategoryMaster,
|
|
||||||
this.description,
|
|
||||||
this.plannedWork,
|
|
||||||
this.completedWork,
|
|
||||||
this.todaysAssigned,
|
|
||||||
this.taskDate,
|
|
||||||
this.tenantId,
|
|
||||||
this.tenant,
|
|
||||||
});
|
|
||||||
|
|
||||||
factory WorkItem.fromJson(Map<String, dynamic> json) {
|
|
||||||
return WorkItem(
|
|
||||||
id: json['id'] as String?,
|
|
||||||
activityId: json['activityId'] as String?,
|
|
||||||
workCategoryId: json['workCategoryId'] as String?,
|
|
||||||
workAreaId: json['workAreaId'] as String?,
|
|
||||||
workArea: json['workArea'] != null
|
|
||||||
? WorkAreaBasic.fromJson(json['workArea'] as Map<String, dynamic>)
|
|
||||||
: null,
|
|
||||||
activityMaster: json['activityMaster'] != null
|
|
||||||
? ActivityMaster.fromJson(
|
|
||||||
json['activityMaster'] as Map<String, dynamic>)
|
|
||||||
: null,
|
|
||||||
workCategoryMaster: json['workCategoryMaster'] != null
|
|
||||||
? WorkCategoryMaster.fromJson(
|
|
||||||
json['workCategoryMaster'] as Map<String, dynamic>)
|
|
||||||
: null,
|
|
||||||
plannedWork: json['plannedWork'] != null
|
|
||||||
? (json['plannedWork'] as num).toDouble()
|
|
||||||
: null,
|
|
||||||
completedWork: json['completedWork'] != null
|
|
||||||
? (json['completedWork'] as num).toDouble()
|
|
||||||
: null,
|
|
||||||
todaysAssigned: json['todaysAssigned'] != null
|
|
||||||
? (json['todaysAssigned'] as num).toDouble()
|
|
||||||
: null,
|
|
||||||
description: json['description'] as String?,
|
|
||||||
taskDate:
|
|
||||||
json['taskDate'] != null ? DateTime.tryParse(json['taskDate']) : null,
|
|
||||||
tenantId: json['tenantId'] as String?,
|
|
||||||
tenant: json['tenant'] != null
|
|
||||||
? Tenant.fromJson(json['tenant'] as Map<String, dynamic>)
|
|
||||||
: null,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class WorkAreaBasic {
|
|
||||||
final String? id;
|
|
||||||
final String? name;
|
|
||||||
|
|
||||||
WorkAreaBasic({this.id, this.name});
|
|
||||||
|
|
||||||
factory WorkAreaBasic.fromJson(Map<String, dynamic> json) {
|
|
||||||
return WorkAreaBasic(
|
|
||||||
id: json['id'] as String?,
|
|
||||||
name: json['name'] as String?,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ActivityMaster {
|
|
||||||
final String? id;
|
|
||||||
final String? name;
|
|
||||||
|
|
||||||
ActivityMaster({this.id, this.name});
|
|
||||||
|
|
||||||
factory ActivityMaster.fromJson(Map<String, dynamic> json) {
|
|
||||||
return ActivityMaster(
|
|
||||||
id: json['id'] as String?,
|
|
||||||
name: json['activityName'] as String?,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class WorkCategoryMaster {
|
|
||||||
final String? id;
|
|
||||||
final String? name;
|
|
||||||
|
|
||||||
WorkCategoryMaster({this.id, this.name});
|
|
||||||
|
|
||||||
factory WorkCategoryMaster.fromJson(Map<String, dynamic> json) {
|
|
||||||
return WorkCategoryMaster(
|
|
||||||
id: json['id'] as String?,
|
|
||||||
name: json['name'] as String?,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class Tenant {
|
|
||||||
final String? id;
|
|
||||||
final String? name;
|
|
||||||
|
|
||||||
Tenant({this.id, this.name});
|
|
||||||
|
|
||||||
factory Tenant.fromJson(Map<String, dynamic> json) {
|
|
||||||
return Tenant(
|
|
||||||
id: json['id'] as String?,
|
|
||||||
name: json['name'] as String?,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,31 +0,0 @@
|
|||||||
class WorkCategoryModel {
|
|
||||||
final String id;
|
|
||||||
final String name;
|
|
||||||
final String description;
|
|
||||||
final bool isSystem;
|
|
||||||
|
|
||||||
WorkCategoryModel({
|
|
||||||
required this.id,
|
|
||||||
required this.name,
|
|
||||||
required this.description,
|
|
||||||
required this.isSystem,
|
|
||||||
});
|
|
||||||
|
|
||||||
factory WorkCategoryModel.fromJson(Map<String, dynamic> json) {
|
|
||||||
return WorkCategoryModel(
|
|
||||||
id: json['id'] ?? '',
|
|
||||||
name: json['name'] ?? '',
|
|
||||||
description: json['description'] ?? '',
|
|
||||||
isSystem: json['isSystem'] ?? false,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
|
||||||
return {
|
|
||||||
'id': id,
|
|
||||||
'name': name,
|
|
||||||
'description': description,
|
|
||||||
'isSystem': isSystem,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,500 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:get/get.dart';
|
|
||||||
import 'package:marco/controller/task_planning/report_task_action_controller.dart';
|
|
||||||
import 'package:marco/helpers/utils/mixins/ui_mixin.dart';
|
|
||||||
import 'package:marco/helpers/widgets/my_spacing.dart';
|
|
||||||
import 'package:marco/helpers/widgets/my_text.dart';
|
|
||||||
import 'package:marco/helpers/widgets/my_text_style.dart';
|
|
||||||
import 'package:marco/helpers/widgets/avatar.dart';
|
|
||||||
import 'package:marco/helpers/widgets/my_team_model_sheet.dart';
|
|
||||||
import 'package:marco/helpers/widgets/image_viewer_dialog.dart';
|
|
||||||
import 'package:marco/model/dailyTaskPlanning/create_task_botom_sheet.dart';
|
|
||||||
import 'package:marco/model/dailyTaskPlanning/report_action_widgets.dart';
|
|
||||||
import 'package:marco/helpers/utils/base_bottom_sheet.dart';
|
|
||||||
|
|
||||||
class ReportActionBottomSheet extends StatefulWidget {
|
|
||||||
final Map<String, dynamic> taskData;
|
|
||||||
final VoidCallback? onCommentSuccess;
|
|
||||||
final String taskDataId;
|
|
||||||
final String workAreaId;
|
|
||||||
final String activityId;
|
|
||||||
final VoidCallback onReportSuccess;
|
|
||||||
|
|
||||||
const ReportActionBottomSheet({
|
|
||||||
super.key,
|
|
||||||
required this.taskData,
|
|
||||||
this.onCommentSuccess,
|
|
||||||
required this.taskDataId,
|
|
||||||
required this.workAreaId,
|
|
||||||
required this.activityId,
|
|
||||||
required this.onReportSuccess,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<ReportActionBottomSheet> createState() =>
|
|
||||||
_ReportActionBottomSheetState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _Member {
|
|
||||||
final String firstName;
|
|
||||||
_Member(this.firstName);
|
|
||||||
}
|
|
||||||
|
|
||||||
class _ReportActionBottomSheetState extends State<ReportActionBottomSheet>
|
|
||||||
with UIMixin {
|
|
||||||
late ReportTaskActionController controller;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
controller = Get.put(
|
|
||||||
ReportTaskActionController(),
|
|
||||||
tag: widget.taskData['taskId'] ?? '',
|
|
||||||
);
|
|
||||||
controller.fetchWorkStatuses();
|
|
||||||
final data = widget.taskData;
|
|
||||||
controller.basicValidator.getController('approved_task')?.text =
|
|
||||||
data['approvedTask']?.toString() ?? '';
|
|
||||||
controller.basicValidator.getController('assigned_date')?.text =
|
|
||||||
data['assignedOn'] ?? '';
|
|
||||||
controller.basicValidator.getController('assigned_by')?.text =
|
|
||||||
data['assignedBy'] ?? '';
|
|
||||||
controller.basicValidator.getController('work_area')?.text =
|
|
||||||
data['location'] ?? '';
|
|
||||||
controller.basicValidator.getController('activity')?.text =
|
|
||||||
data['activity'] ?? '';
|
|
||||||
controller.basicValidator.getController('planned_work')?.text =
|
|
||||||
data['plannedWork'] ?? '';
|
|
||||||
controller.basicValidator.getController('completed_work')?.text =
|
|
||||||
data['completedWork'] ?? '';
|
|
||||||
controller.basicValidator.getController('team_members')?.text =
|
|
||||||
(data['teamMembers'] as List<dynamic>).join(', ');
|
|
||||||
controller.basicValidator.getController('assigned')?.text =
|
|
||||||
data['assigned'] ?? '';
|
|
||||||
controller.basicValidator.getController('task_id')?.text =
|
|
||||||
widget.taskDataId;
|
|
||||||
controller.basicValidator.getController('comment')?.clear();
|
|
||||||
controller.selectedImages.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return GetBuilder<ReportTaskActionController>(
|
|
||||||
tag: widget.taskData['taskId'] ?? '',
|
|
||||||
builder: (controller) {
|
|
||||||
return BaseBottomSheet(
|
|
||||||
title: "Take Report Action",
|
|
||||||
isSubmitting: controller.isLoading.value,
|
|
||||||
onCancel: () => Navigator.of(context).pop(),
|
|
||||||
onSubmit: () async {}, // not used since buttons moved
|
|
||||||
showButtons: false, // disable internal buttons
|
|
||||||
child: _buildForm(context, controller),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildForm(
|
|
||||||
BuildContext context, ReportTaskActionController controller) {
|
|
||||||
return Form(
|
|
||||||
key: controller.basicValidator.formKey,
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
// 📋 Task Details
|
|
||||||
buildRow("Assigned By",
|
|
||||||
controller.basicValidator.getController('assigned_by')?.text,
|
|
||||||
icon: Icons.person_outline),
|
|
||||||
buildRow("Work Area",
|
|
||||||
controller.basicValidator.getController('work_area')?.text,
|
|
||||||
icon: Icons.place_outlined),
|
|
||||||
buildRow("Activity",
|
|
||||||
controller.basicValidator.getController('activity')?.text,
|
|
||||||
icon: Icons.assignment_outlined),
|
|
||||||
buildRow("Planned Work",
|
|
||||||
controller.basicValidator.getController('planned_work')?.text,
|
|
||||||
icon: Icons.schedule_outlined),
|
|
||||||
buildRow("Completed Work",
|
|
||||||
controller.basicValidator.getController('completed_work')?.text,
|
|
||||||
icon: Icons.done_all_outlined),
|
|
||||||
buildTeamMembers(),
|
|
||||||
MySpacing.height(8),
|
|
||||||
|
|
||||||
// ✅ Approved Task Field
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Icon(Icons.check_circle_outline,
|
|
||||||
size: 18, color: Colors.grey[700]),
|
|
||||||
MySpacing.width(8),
|
|
||||||
MyText.titleSmall("Approved Task:", fontWeight: 600),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
MySpacing.height(10),
|
|
||||||
TextFormField(
|
|
||||||
controller:
|
|
||||||
controller.basicValidator.getController('approved_task'),
|
|
||||||
keyboardType: TextInputType.number,
|
|
||||||
validator: (value) {
|
|
||||||
if (value == null || value.isEmpty) return 'Required';
|
|
||||||
if (int.tryParse(value) == null) return 'Must be a number';
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
decoration: InputDecoration(
|
|
||||||
hintText: "eg: 5",
|
|
||||||
hintStyle: MyTextStyle.bodySmall(xMuted: true),
|
|
||||||
border: outlineInputBorder,
|
|
||||||
contentPadding: MySpacing.all(16),
|
|
||||||
floatingLabelBehavior: FloatingLabelBehavior.never,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
MySpacing.height(10),
|
|
||||||
if ((widget.taskData['reportedPreSignedUrls'] as List<dynamic>?)
|
|
||||||
?.isNotEmpty ==
|
|
||||||
true)
|
|
||||||
buildReportedImagesSection(
|
|
||||||
imageUrls: List<String>.from(
|
|
||||||
widget.taskData['reportedPreSignedUrls'] ?? []),
|
|
||||||
context: context,
|
|
||||||
),
|
|
||||||
|
|
||||||
MySpacing.height(10),
|
|
||||||
MyText.titleSmall("Report Actions", fontWeight: 600),
|
|
||||||
MySpacing.height(10),
|
|
||||||
|
|
||||||
Obx(() {
|
|
||||||
if (controller.isLoadingWorkStatus.value)
|
|
||||||
return const CircularProgressIndicator();
|
|
||||||
return PopupMenuButton<String>(
|
|
||||||
onSelected: (String value) {
|
|
||||||
controller.selectedWorkStatusName.value = value;
|
|
||||||
controller.showAddTaskCheckbox.value = true;
|
|
||||||
},
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(12)),
|
|
||||||
itemBuilder: (BuildContext context) {
|
|
||||||
return controller.workStatus.map((status) {
|
|
||||||
return PopupMenuItem<String>(
|
|
||||||
value: status.name,
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Radio<String>(
|
|
||||||
value: status.name,
|
|
||||||
groupValue: controller.selectedWorkStatusName.value,
|
|
||||||
onChanged: (_) => Navigator.pop(context, status.name),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
MyText.bodySmall(status.name),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}).toList();
|
|
||||||
},
|
|
||||||
child: Container(
|
|
||||||
padding: MySpacing.xy(16, 12),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
border: Border.all(color: Colors.grey.shade400),
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
MyText.bodySmall(
|
|
||||||
controller.selectedWorkStatusName.value.isEmpty
|
|
||||||
? "Select Work Status"
|
|
||||||
: controller.selectedWorkStatusName.value,
|
|
||||||
color: Colors.black87,
|
|
||||||
),
|
|
||||||
const Icon(Icons.arrow_drop_down, size: 20),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
|
|
||||||
MySpacing.height(10),
|
|
||||||
|
|
||||||
Obx(() {
|
|
||||||
if (!controller.showAddTaskCheckbox.value)
|
|
||||||
return const SizedBox.shrink();
|
|
||||||
return Theme(
|
|
||||||
data: Theme.of(context).copyWith(
|
|
||||||
checkboxTheme: CheckboxThemeData(
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(4),
|
|
||||||
),
|
|
||||||
side: const BorderSide(
|
|
||||||
color: Colors.black, width: 2),
|
|
||||||
fillColor: MaterialStateProperty.resolveWith<Color>((states) {
|
|
||||||
if (states.contains(MaterialState.selected)) {
|
|
||||||
return Colors.blueAccent;
|
|
||||||
}
|
|
||||||
return Colors.white;
|
|
||||||
}),
|
|
||||||
checkColor:
|
|
||||||
MaterialStateProperty.all(Colors.white),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: CheckboxListTile(
|
|
||||||
title: MyText.titleSmall("Add new task", fontWeight: 600),
|
|
||||||
value: controller.isAddTaskChecked.value,
|
|
||||||
onChanged: (val) =>
|
|
||||||
controller.isAddTaskChecked.value = val ?? false,
|
|
||||||
controlAffinity: ListTileControlAffinity.leading,
|
|
||||||
contentPadding: EdgeInsets.zero,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
|
|
||||||
MySpacing.height(24),
|
|
||||||
|
|
||||||
// ✏️ Comment Field
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Icon(Icons.comment_outlined, size: 18, color: Colors.grey[700]),
|
|
||||||
MySpacing.width(8),
|
|
||||||
MyText.titleSmall("Comment:", fontWeight: 600),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
MySpacing.height(8),
|
|
||||||
TextFormField(
|
|
||||||
validator: controller.basicValidator.getValidation('comment'),
|
|
||||||
controller: controller.basicValidator.getController('comment'),
|
|
||||||
keyboardType: TextInputType.text,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
hintText: "eg: Work done successfully",
|
|
||||||
hintStyle: MyTextStyle.bodySmall(xMuted: true),
|
|
||||||
border: outlineInputBorder,
|
|
||||||
contentPadding: MySpacing.all(16),
|
|
||||||
floatingLabelBehavior: FloatingLabelBehavior.never,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
MySpacing.height(16),
|
|
||||||
|
|
||||||
// 📸 Image Attachments
|
|
||||||
Row(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Icon(Icons.camera_alt_outlined,
|
|
||||||
size: 18, color: Colors.grey[700]),
|
|
||||||
MySpacing.width(8),
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
MyText.titleSmall("Attach Photos:", fontWeight: 600),
|
|
||||||
MySpacing.height(12),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
Obx(() {
|
|
||||||
final images = controller.selectedImages;
|
|
||||||
return buildImagePickerSection(
|
|
||||||
images: images,
|
|
||||||
onCameraTap: () => controller.pickImages(fromCamera: true),
|
|
||||||
onUploadTap: () => controller.pickImages(fromCamera: false),
|
|
||||||
onRemoveImage: (index) => controller.removeImageAt(index),
|
|
||||||
onPreviewImage: (index) {
|
|
||||||
showDialog(
|
|
||||||
context: context,
|
|
||||||
builder: (_) => ImageViewerDialog(
|
|
||||||
imageSources: images,
|
|
||||||
initialIndex: index,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
|
|
||||||
MySpacing.height(12),
|
|
||||||
|
|
||||||
// ✅ Submit/Cancel Buttons moved here
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: ElevatedButton.icon(
|
|
||||||
onPressed: () => Navigator.of(context).pop(),
|
|
||||||
icon: const Icon(Icons.close, color: Colors.white),
|
|
||||||
label: MyText.bodyMedium("Cancel",
|
|
||||||
color: Colors.white, fontWeight: 600),
|
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
backgroundColor: Colors.grey,
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(12)),
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 12),
|
|
||||||
Expanded(
|
|
||||||
child: ElevatedButton.icon(
|
|
||||||
onPressed: controller.isLoading.value
|
|
||||||
? null
|
|
||||||
: () async {
|
|
||||||
if (controller.basicValidator.validateForm()) {
|
|
||||||
final selectedStatusName =
|
|
||||||
controller.selectedWorkStatusName.value;
|
|
||||||
final selectedStatus = controller.workStatus
|
|
||||||
.firstWhereOrNull(
|
|
||||||
(s) => s.name == selectedStatusName);
|
|
||||||
final reportActionId =
|
|
||||||
selectedStatus?.id.toString() ?? '';
|
|
||||||
final approvedTaskCount = controller.basicValidator
|
|
||||||
.getController('approved_task')
|
|
||||||
?.text
|
|
||||||
.trim() ??
|
|
||||||
'';
|
|
||||||
|
|
||||||
final shouldShowAddTaskSheet =
|
|
||||||
controller.isAddTaskChecked.value;
|
|
||||||
|
|
||||||
final success = await controller.approveTask(
|
|
||||||
projectId: controller.basicValidator
|
|
||||||
.getController('task_id')
|
|
||||||
?.text ??
|
|
||||||
'',
|
|
||||||
comment: controller.basicValidator
|
|
||||||
.getController('comment')
|
|
||||||
?.text ??
|
|
||||||
'',
|
|
||||||
images: controller.selectedImages,
|
|
||||||
reportActionId: reportActionId,
|
|
||||||
approvedTaskCount: approvedTaskCount,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (success) {
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
if (shouldShowAddTaskSheet) {
|
|
||||||
await Future.delayed(
|
|
||||||
const Duration(milliseconds: 100));
|
|
||||||
showCreateTaskBottomSheet(
|
|
||||||
workArea: widget.taskData['location'] ?? '',
|
|
||||||
activity: widget.taskData['activity'] ?? '',
|
|
||||||
completedWork:
|
|
||||||
widget.taskData['completedWork'] ?? '',
|
|
||||||
unit: widget.taskData['unit'] ?? '',
|
|
||||||
parentTaskId: widget.taskDataId,
|
|
||||||
plannedTask: int.tryParse(
|
|
||||||
widget.taskData['plannedWork'] ??
|
|
||||||
'0') ??
|
|
||||||
0,
|
|
||||||
activityId: widget.activityId,
|
|
||||||
workAreaId: widget.workAreaId,
|
|
||||||
onSubmit: () => Navigator.of(context).pop(),
|
|
||||||
onCategoryChanged: (category) {},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
widget.onReportSuccess.call();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
icon: const Icon(Icons.check_circle_outline,
|
|
||||||
color: Colors.white),
|
|
||||||
label: MyText.bodyMedium(
|
|
||||||
controller.isLoading.value ? "Submitting..." : "Submit",
|
|
||||||
color: Colors.white,
|
|
||||||
fontWeight: 600,
|
|
||||||
),
|
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
backgroundColor: Colors.indigo,
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(12)),
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
|
|
||||||
MySpacing.height(12),
|
|
||||||
|
|
||||||
// 💬 Previous Comments List (only below submit)
|
|
||||||
if ((widget.taskData['taskComments'] as List<dynamic>?)?.isNotEmpty ==
|
|
||||||
true) ...[
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
MySpacing.width(10),
|
|
||||||
Icon(Icons.chat_bubble_outline,
|
|
||||||
size: 18, color: Colors.grey[700]),
|
|
||||||
MySpacing.width(8),
|
|
||||||
MyText.titleSmall("Comments", fontWeight: 600),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
MySpacing.height(12),
|
|
||||||
buildCommentList(
|
|
||||||
List<Map<String, dynamic>>.from(
|
|
||||||
widget.taskData['taskComments'] as List),
|
|
||||||
context,
|
|
||||||
timeAgo,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget buildTeamMembers() {
|
|
||||||
final teamMembersText =
|
|
||||||
controller.basicValidator.getController('team_members')?.text ?? '';
|
|
||||||
final members = teamMembersText
|
|
||||||
.split(',')
|
|
||||||
.map((e) => e.trim())
|
|
||||||
.where((e) => e.isNotEmpty)
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.only(bottom: 16),
|
|
||||||
child: Row(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
MyText.titleSmall("Team Members:", fontWeight: 600),
|
|
||||||
MySpacing.width(12),
|
|
||||||
GestureDetector(
|
|
||||||
onTap: () {
|
|
||||||
TeamBottomSheet.show(
|
|
||||||
context: context,
|
|
||||||
teamMembers: members.map((name) => _Member(name)).toList(),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child: SizedBox(
|
|
||||||
height: 32,
|
|
||||||
width: 100,
|
|
||||||
child: Stack(
|
|
||||||
children: [
|
|
||||||
for (int i = 0; i < members.length.clamp(0, 3); i++)
|
|
||||||
Positioned(
|
|
||||||
left: i * 24.0,
|
|
||||||
child: Tooltip(
|
|
||||||
message: members[i],
|
|
||||||
child: Avatar(
|
|
||||||
firstName: members[i],
|
|
||||||
lastName: '',
|
|
||||||
size: 32,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (members.length > 3)
|
|
||||||
Positioned(
|
|
||||||
left: 2 * 24.0,
|
|
||||||
child: CircleAvatar(
|
|
||||||
radius: 16,
|
|
||||||
backgroundColor: Colors.grey.shade300,
|
|
||||||
child: MyText.bodyMedium(
|
|
||||||
'+${members.length - 3}',
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 12, color: Colors.black87),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,392 +0,0 @@
|
|||||||
import 'dart:io';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:marco/helpers/widgets/my_button.dart';
|
|
||||||
import 'package:marco/helpers/widgets/my_spacing.dart';
|
|
||||||
import 'package:marco/helpers/widgets/my_text.dart';
|
|
||||||
import 'package:marco/helpers/widgets/image_viewer_dialog.dart';
|
|
||||||
import 'package:marco/helpers/widgets/avatar.dart';
|
|
||||||
import 'package:get/get.dart';
|
|
||||||
|
|
||||||
/// Show labeled row with optional icon
|
|
||||||
Widget buildRow(String label, String? value, {IconData? icon}) {
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.only(bottom: 16),
|
|
||||||
child: Row(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
if (icon != null)
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(right: 8.0, top: 2),
|
|
||||||
child: Icon(icon, size: 18, color: Colors.grey[700]),
|
|
||||||
),
|
|
||||||
MyText.titleSmall("$label:", fontWeight: 600),
|
|
||||||
MySpacing.width(12),
|
|
||||||
Expanded(
|
|
||||||
child: MyText.bodyMedium(value?.isNotEmpty == true ? value! : "-"),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Show uploaded network images
|
|
||||||
Widget buildReportedImagesSection({
|
|
||||||
required List<String> imageUrls,
|
|
||||||
required BuildContext context,
|
|
||||||
String title = "Reported Images",
|
|
||||||
}) {
|
|
||||||
if (imageUrls.isEmpty) return const SizedBox();
|
|
||||||
|
|
||||||
return Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
MySpacing.height(8),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Icon(Icons.image_outlined, size: 18, color: Colors.grey[700]),
|
|
||||||
MySpacing.width(8),
|
|
||||||
MyText.titleSmall(title, fontWeight: 600),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
MySpacing.height(8),
|
|
||||||
SizedBox(
|
|
||||||
height: 70,
|
|
||||||
child: ListView.separated(
|
|
||||||
scrollDirection: Axis.horizontal,
|
|
||||||
itemCount: imageUrls.length,
|
|
||||||
separatorBuilder: (_, __) => const SizedBox(width: 12),
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
final url = imageUrls[index];
|
|
||||||
return GestureDetector(
|
|
||||||
onTap: () {
|
|
||||||
showDialog(
|
|
||||||
context: context,
|
|
||||||
builder: (_) => ImageViewerDialog(
|
|
||||||
imageSources: imageUrls,
|
|
||||||
initialIndex: index,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child: ClipRRect(
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
child: Image.network(
|
|
||||||
url,
|
|
||||||
width: 70,
|
|
||||||
height: 70,
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
errorBuilder: (context, error, stackTrace) => Container(
|
|
||||||
width: 70,
|
|
||||||
height: 70,
|
|
||||||
color: Colors.grey.shade200,
|
|
||||||
child: Icon(Icons.broken_image, color: Colors.grey[600]),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
MySpacing.height(16),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Local image picker preview (with file images)
|
|
||||||
Widget buildImagePickerSection({
|
|
||||||
required List<File> images,
|
|
||||||
required VoidCallback onCameraTap,
|
|
||||||
required VoidCallback onUploadTap,
|
|
||||||
required void Function(int index) onRemoveImage,
|
|
||||||
required void Function(int initialIndex) onPreviewImage,
|
|
||||||
}) {
|
|
||||||
return Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
if (images.isEmpty)
|
|
||||||
Container(
|
|
||||||
height: 70,
|
|
||||||
width: double.infinity,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
border: Border.all(color: Colors.grey.shade300, width: 2),
|
|
||||||
color: Colors.grey.shade100,
|
|
||||||
),
|
|
||||||
child: Center(
|
|
||||||
child: Icon(Icons.photo_camera_outlined,
|
|
||||||
size: 48, color: Colors.grey.shade400),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
else
|
|
||||||
SizedBox(
|
|
||||||
height: 70,
|
|
||||||
child: ListView.separated(
|
|
||||||
scrollDirection: Axis.horizontal,
|
|
||||||
itemCount: images.length,
|
|
||||||
separatorBuilder: (_, __) => const SizedBox(width: 12),
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
final file = images[index];
|
|
||||||
return Stack(
|
|
||||||
children: [
|
|
||||||
GestureDetector(
|
|
||||||
onTap: () => onPreviewImage(index),
|
|
||||||
child: ClipRRect(
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
child: Image.file(
|
|
||||||
file,
|
|
||||||
height: 70,
|
|
||||||
width: 70,
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Positioned(
|
|
||||||
top: 4,
|
|
||||||
right: 4,
|
|
||||||
child: GestureDetector(
|
|
||||||
onTap: () => onRemoveImage(index),
|
|
||||||
child: Container(
|
|
||||||
decoration: const BoxDecoration(
|
|
||||||
color: Colors.black54,
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
),
|
|
||||||
child: const Icon(Icons.close,
|
|
||||||
size: 20, color: Colors.white),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
MySpacing.height(16),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: MyButton.outlined(
|
|
||||||
onPressed: onCameraTap,
|
|
||||||
padding: MySpacing.xy(12, 10),
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
const Icon(Icons.camera_alt,
|
|
||||||
size: 16, color: Colors.blueAccent),
|
|
||||||
MySpacing.width(6),
|
|
||||||
MyText.bodySmall('Capture', color: Colors.blueAccent),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
MySpacing.width(12),
|
|
||||||
Expanded(
|
|
||||||
child: MyButton.outlined(
|
|
||||||
onPressed: onUploadTap,
|
|
||||||
padding: MySpacing.xy(12, 10),
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
const Icon(Icons.upload_file,
|
|
||||||
size: 16, color: Colors.blueAccent),
|
|
||||||
MySpacing.width(6),
|
|
||||||
MyText.bodySmall('Upload', color: Colors.blueAccent),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Comment list widget
|
|
||||||
Widget buildCommentList(
|
|
||||||
List<Map<String, dynamic>> comments, BuildContext context, String Function(String) timeAgo) {
|
|
||||||
comments.sort((a, b) {
|
|
||||||
final aDate = DateTime.tryParse(a['date'] ?? '') ??
|
|
||||||
DateTime.fromMillisecondsSinceEpoch(0);
|
|
||||||
final bDate = DateTime.tryParse(b['date'] ?? '') ??
|
|
||||||
DateTime.fromMillisecondsSinceEpoch(0);
|
|
||||||
return bDate.compareTo(aDate); // newest first
|
|
||||||
});
|
|
||||||
|
|
||||||
return SizedBox(
|
|
||||||
height: 300,
|
|
||||||
child: ListView.builder(
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
|
||||||
itemCount: comments.length,
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
final comment = comments[index];
|
|
||||||
final commentText = comment['text'] ?? '-';
|
|
||||||
final commentedBy = comment['commentedBy'] ?? 'Unknown';
|
|
||||||
final relativeTime = timeAgo(comment['date'] ?? '');
|
|
||||||
final imageUrls = List<String>.from(comment['preSignedUrls'] ?? []);
|
|
||||||
|
|
||||||
return Container(
|
|
||||||
margin: const EdgeInsets.symmetric(vertical: 8),
|
|
||||||
padding: const EdgeInsets.all(12),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.grey.shade200,
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Avatar(
|
|
||||||
firstName: commentedBy.split(' ').first,
|
|
||||||
lastName: commentedBy.split(' ').length > 1
|
|
||||||
? commentedBy.split(' ').last
|
|
||||||
: '',
|
|
||||||
size: 32,
|
|
||||||
),
|
|
||||||
const SizedBox(width: 12),
|
|
||||||
Expanded(
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
MyText.bodyMedium(commentedBy,
|
|
||||||
fontWeight: 700, color: Colors.black87),
|
|
||||||
MyText.bodySmall(
|
|
||||||
relativeTime,
|
|
||||||
fontSize: 12,
|
|
||||||
color: Colors.black54,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
MyText.bodyMedium(commentText,
|
|
||||||
fontWeight: 500, color: Colors.black87),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
if (imageUrls.isNotEmpty) ...[
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Icon(Icons.attach_file_outlined,
|
|
||||||
size: 18, color: Colors.grey[700]),
|
|
||||||
MySpacing.width(8),
|
|
||||||
MyText.bodyMedium('Attachments',
|
|
||||||
fontWeight: 600, color: Colors.black87),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
SizedBox(
|
|
||||||
height: 60,
|
|
||||||
child: ListView.separated(
|
|
||||||
scrollDirection: Axis.horizontal,
|
|
||||||
itemCount: imageUrls.length,
|
|
||||||
itemBuilder: (context, imageIndex) {
|
|
||||||
final imageUrl = imageUrls[imageIndex];
|
|
||||||
return GestureDetector(
|
|
||||||
onTap: () {
|
|
||||||
showDialog(
|
|
||||||
context: context,
|
|
||||||
builder: (_) => ImageViewerDialog(
|
|
||||||
imageSources: imageUrls,
|
|
||||||
initialIndex: imageIndex,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child: ClipRRect(
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
child: Image.network(
|
|
||||||
imageUrl,
|
|
||||||
width: 60,
|
|
||||||
height: 60,
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
separatorBuilder: (_, __) => const SizedBox(width: 12),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Cancel + Submit buttons
|
|
||||||
Widget buildCommentActionButtons({
|
|
||||||
required VoidCallback onCancel,
|
|
||||||
required Future<void> Function() onSubmit,
|
|
||||||
required RxBool isLoading,
|
|
||||||
}) {
|
|
||||||
return Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: OutlinedButton.icon(
|
|
||||||
onPressed: onCancel,
|
|
||||||
icon: const Icon(Icons.close, color: Colors.red, size: 18),
|
|
||||||
label:
|
|
||||||
MyText.bodyMedium("Cancel", color: Colors.red, fontWeight: 600),
|
|
||||||
style: OutlinedButton.styleFrom(
|
|
||||||
side: const BorderSide(color: Colors.red),
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 14),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 16),
|
|
||||||
Expanded(
|
|
||||||
child: Obx(() {
|
|
||||||
return ElevatedButton.icon(
|
|
||||||
onPressed: isLoading.value ? null : () => onSubmit(),
|
|
||||||
icon: isLoading.value
|
|
||||||
? const SizedBox(
|
|
||||||
width: 16,
|
|
||||||
height: 16,
|
|
||||||
child: CircularProgressIndicator(
|
|
||||||
strokeWidth: 2,
|
|
||||||
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: const Icon(Icons.send, color: Colors.white, size: 18),
|
|
||||||
label: isLoading.value
|
|
||||||
? const SizedBox()
|
|
||||||
: MyText.bodyMedium("Submit",
|
|
||||||
color: Colors.white, fontWeight: 600),
|
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
backgroundColor: Colors.indigo,
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 14),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Converts a UTC timestamp to a relative time string
|
|
||||||
String timeAgo(String dateString) {
|
|
||||||
try {
|
|
||||||
DateTime date = DateTime.parse(dateString + "Z").toLocal();
|
|
||||||
final now = DateTime.now();
|
|
||||||
final difference = now.difference(date);
|
|
||||||
if (difference.inDays > 8) {
|
|
||||||
return "${date.day.toString().padLeft(2, '0')}-${date.month.toString().padLeft(2, '0')}-${date.year}";
|
|
||||||
} else if (difference.inDays >= 1) {
|
|
||||||
return '${difference.inDays} day${difference.inDays > 1 ? 's' : ''} ago';
|
|
||||||
} else if (difference.inHours >= 1) {
|
|
||||||
return '${difference.inHours} hr${difference.inHours > 1 ? 's' : ''} ago';
|
|
||||||
} else if (difference.inMinutes >= 1) {
|
|
||||||
return '${difference.inMinutes} min${difference.inMinutes > 1 ? 's' : ''} ago';
|
|
||||||
} else {
|
|
||||||
return 'just now';
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,310 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:get/get.dart';
|
|
||||||
import 'package:marco/controller/task_planning/report_task_controller.dart';
|
|
||||||
import 'package:marco/helpers/utils/mixins/ui_mixin.dart';
|
|
||||||
import 'package:marco/helpers/widgets/my_button.dart';
|
|
||||||
import 'package:marco/helpers/widgets/my_spacing.dart';
|
|
||||||
import 'package:marco/helpers/widgets/my_text.dart';
|
|
||||||
import 'package:marco/helpers/widgets/my_text_style.dart';
|
|
||||||
import 'package:marco/helpers/utils/base_bottom_sheet.dart';
|
|
||||||
|
|
||||||
class ReportTaskBottomSheet extends StatefulWidget {
|
|
||||||
final Map<String, dynamic> taskData;
|
|
||||||
final VoidCallback? onReportSuccess;
|
|
||||||
|
|
||||||
const ReportTaskBottomSheet({
|
|
||||||
super.key,
|
|
||||||
required this.taskData,
|
|
||||||
this.onReportSuccess,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<ReportTaskBottomSheet> createState() => _ReportTaskBottomSheetState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _ReportTaskBottomSheetState extends State<ReportTaskBottomSheet>
|
|
||||||
with UIMixin {
|
|
||||||
late final ReportTaskController controller;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
controller = Get.put(
|
|
||||||
ReportTaskController(),
|
|
||||||
tag: widget.taskData['taskId'] ?? UniqueKey().toString(),
|
|
||||||
);
|
|
||||||
_preFillFormFields();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _preFillFormFields() {
|
|
||||||
final data = widget.taskData;
|
|
||||||
final v = controller.basicValidator;
|
|
||||||
|
|
||||||
v.getController('assigned_date')?.text = data['assignedOn'] ?? '';
|
|
||||||
v.getController('assigned_by')?.text = data['assignedBy'] ?? '';
|
|
||||||
v.getController('work_area')?.text = data['location'] ?? '';
|
|
||||||
v.getController('activity')?.text = data['activity'] ?? '';
|
|
||||||
v.getController('team_size')?.text = data['teamSize']?.toString() ?? '';
|
|
||||||
v.getController('assigned')?.text = data['assigned'] ?? '';
|
|
||||||
v.getController('task_id')?.text = data['taskId'] ?? '';
|
|
||||||
v.getController('completed_work')?.clear();
|
|
||||||
v.getController('comment')?.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Obx(() {
|
|
||||||
return BaseBottomSheet(
|
|
||||||
title: "Report Task",
|
|
||||||
isSubmitting: controller.reportStatus.value == ApiStatus.loading,
|
|
||||||
onCancel: () => Navigator.of(context).pop(),
|
|
||||||
onSubmit: _handleSubmit,
|
|
||||||
child: Form(
|
|
||||||
key: controller.basicValidator.formKey,
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
_buildRow("Assigned Date", controller.basicValidator.getController('assigned_date')?.text),
|
|
||||||
_buildRow("Assigned By", controller.basicValidator.getController('assigned_by')?.text),
|
|
||||||
_buildRow("Work Area", controller.basicValidator.getController('work_area')?.text),
|
|
||||||
_buildRow("Activity", controller.basicValidator.getController('activity')?.text),
|
|
||||||
_buildRow("Team Size", controller.basicValidator.getController('team_size')?.text),
|
|
||||||
_buildRow(
|
|
||||||
"Assigned",
|
|
||||||
"${controller.basicValidator.getController('assigned')?.text ?? '-'} "
|
|
||||||
"of ${widget.taskData['pendingWork'] ?? '-'} Pending",
|
|
||||||
),
|
|
||||||
_buildCompletedWorkField(),
|
|
||||||
_buildCommentField(),
|
|
||||||
Obx(() => _buildImageSection()),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _handleSubmit() async {
|
|
||||||
final v = controller.basicValidator;
|
|
||||||
|
|
||||||
if (v.validateForm()) {
|
|
||||||
final success = await controller.reportTask(
|
|
||||||
projectId: v.getController('task_id')?.text ?? '',
|
|
||||||
comment: v.getController('comment')?.text ?? '',
|
|
||||||
completedTask: int.tryParse(v.getController('completed_work')?.text ?? '') ?? 0,
|
|
||||||
checklist: [],
|
|
||||||
reportedDate: DateTime.now(),
|
|
||||||
images: controller.selectedImages,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (success) {
|
|
||||||
widget.onReportSuccess?.call();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildRow(String label, String? value) {
|
|
||||||
final icons = {
|
|
||||||
"Assigned Date": Icons.calendar_today_outlined,
|
|
||||||
"Assigned By": Icons.person_outline,
|
|
||||||
"Work Area": Icons.place_outlined,
|
|
||||||
"Activity": Icons.run_circle_outlined,
|
|
||||||
"Team Size": Icons.group_outlined,
|
|
||||||
"Assigned": Icons.assignment_turned_in_outlined,
|
|
||||||
};
|
|
||||||
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.only(bottom: 16),
|
|
||||||
child: Row(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Icon(icons[label] ?? Icons.info_outline, size: 18, color: Colors.grey[700]),
|
|
||||||
MySpacing.width(8),
|
|
||||||
MyText.titleSmall("$label:", fontWeight: 600),
|
|
||||||
MySpacing.width(12),
|
|
||||||
Expanded(
|
|
||||||
child: MyText.bodyMedium(value?.trim().isNotEmpty == true ? value!.trim() : "-"),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildCompletedWorkField() {
|
|
||||||
final pending = widget.taskData['pendingWork'] ?? 0;
|
|
||||||
|
|
||||||
return Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Icon(Icons.work_outline, size: 18, color: Colors.grey[700]),
|
|
||||||
MySpacing.width(8),
|
|
||||||
MyText.titleSmall("Completed Work:", fontWeight: 600),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
MySpacing.height(8),
|
|
||||||
TextFormField(
|
|
||||||
controller: controller.basicValidator.getController('completed_work'),
|
|
||||||
keyboardType: TextInputType.number,
|
|
||||||
validator: (value) {
|
|
||||||
if (value == null || value.trim().isEmpty) return 'Please enter completed work';
|
|
||||||
final completed = int.tryParse(value.trim());
|
|
||||||
if (completed == null) return 'Enter a valid number';
|
|
||||||
if (completed > pending) return 'Completed work cannot exceed pending work $pending';
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
decoration: InputDecoration(
|
|
||||||
hintText: "eg: 10",
|
|
||||||
hintStyle: MyTextStyle.bodySmall(xMuted: true),
|
|
||||||
border: outlineInputBorder,
|
|
||||||
enabledBorder: outlineInputBorder,
|
|
||||||
focusedBorder: focusedInputBorder,
|
|
||||||
contentPadding: MySpacing.all(16),
|
|
||||||
isCollapsed: true,
|
|
||||||
floatingLabelBehavior: FloatingLabelBehavior.never,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
MySpacing.height(24),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildCommentField() {
|
|
||||||
return Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Icon(Icons.comment_outlined, size: 18, color: Colors.grey[700]),
|
|
||||||
MySpacing.width(8),
|
|
||||||
MyText.titleSmall("Comment:", fontWeight: 600),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
MySpacing.height(8),
|
|
||||||
TextFormField(
|
|
||||||
controller: controller.basicValidator.getController('comment'),
|
|
||||||
validator: controller.basicValidator.getValidation('comment'),
|
|
||||||
keyboardType: TextInputType.text,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
hintText: "eg: Work done successfully",
|
|
||||||
hintStyle: MyTextStyle.bodySmall(xMuted: true),
|
|
||||||
border: outlineInputBorder,
|
|
||||||
enabledBorder: outlineInputBorder,
|
|
||||||
focusedBorder: focusedInputBorder,
|
|
||||||
contentPadding: MySpacing.all(16),
|
|
||||||
isCollapsed: true,
|
|
||||||
floatingLabelBehavior: FloatingLabelBehavior.never,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
MySpacing.height(24),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildImageSection() {
|
|
||||||
final images = controller.selectedImages;
|
|
||||||
|
|
||||||
return Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Icon(Icons.camera_alt_outlined, size: 18, color: Colors.grey[700]),
|
|
||||||
MySpacing.width(8),
|
|
||||||
MyText.titleSmall("Attach Photos:", fontWeight: 600),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
MySpacing.height(12),
|
|
||||||
if (images.isEmpty)
|
|
||||||
Container(
|
|
||||||
height: 70,
|
|
||||||
width: double.infinity,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
border: Border.all(color: Colors.grey.shade300, width: 2),
|
|
||||||
color: Colors.grey.shade100,
|
|
||||||
),
|
|
||||||
child: Center(
|
|
||||||
child: Icon(Icons.photo_camera_outlined, size: 48, color: Colors.grey.shade400),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
else
|
|
||||||
SizedBox(
|
|
||||||
height: 70,
|
|
||||||
child: ListView.separated(
|
|
||||||
scrollDirection: Axis.horizontal,
|
|
||||||
itemCount: images.length,
|
|
||||||
separatorBuilder: (_, __) => MySpacing.width(12),
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
final file = images[index];
|
|
||||||
return Stack(
|
|
||||||
children: [
|
|
||||||
GestureDetector(
|
|
||||||
onTap: () {
|
|
||||||
showDialog(
|
|
||||||
context: context,
|
|
||||||
builder: (_) => Dialog(
|
|
||||||
child: InteractiveViewer(child: Image.file(file)),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child: ClipRRect(
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
child: Image.file(file, height: 70, width: 70, fit: BoxFit.cover),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Positioned(
|
|
||||||
top: 4,
|
|
||||||
right: 4,
|
|
||||||
child: GestureDetector(
|
|
||||||
onTap: () => controller.removeImageAt(index),
|
|
||||||
child: Container(
|
|
||||||
decoration: BoxDecoration(color: Colors.black54, shape: BoxShape.circle),
|
|
||||||
child: const Icon(Icons.close, size: 20, color: Colors.white),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
MySpacing.height(16),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: MyButton.outlined(
|
|
||||||
onPressed: () => controller.pickImages(fromCamera: true),
|
|
||||||
padding: MySpacing.xy(12, 10),
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
const Icon(Icons.camera_alt, size: 16, color: Colors.blueAccent),
|
|
||||||
MySpacing.width(6),
|
|
||||||
MyText.bodySmall('Capture', color: Colors.blueAccent),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
MySpacing.width(12),
|
|
||||||
Expanded(
|
|
||||||
child: MyButton.outlined(
|
|
||||||
onPressed: () => controller.pickImages(fromCamera: false),
|
|
||||||
padding: MySpacing.xy(12, 10),
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
const Icon(Icons.upload_file, size: 16, color: Colors.blueAccent),
|
|
||||||
MySpacing.width(6),
|
|
||||||
MyText.bodySmall('Upload', color: Colors.blueAccent),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,210 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:intl/intl.dart';
|
|
||||||
import 'package:marco/model/dailyTaskPlanning/comment_task_bottom_sheet.dart';
|
|
||||||
import 'package:marco/model/dailyTaskPlanning/report_task_bottom_sheet.dart';
|
|
||||||
import 'package:marco/model/dailyTaskPlanning/report_action_bottom_sheet.dart';
|
|
||||||
|
|
||||||
class TaskActionButtons {
|
|
||||||
static Widget reportButton({
|
|
||||||
required BuildContext context,
|
|
||||||
required dynamic task,
|
|
||||||
required int completed,
|
|
||||||
required VoidCallback refreshCallback,
|
|
||||||
}) {
|
|
||||||
return OutlinedButton.icon(
|
|
||||||
icon: const Icon(Icons.report, size: 18, color: Colors.blueAccent),
|
|
||||||
label: const Text('Report', style: TextStyle(color: Colors.blueAccent)),
|
|
||||||
style: OutlinedButton.styleFrom(
|
|
||||||
side: const BorderSide(color: Colors.blueAccent),
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
|
||||||
textStyle: const TextStyle(fontSize: 14),
|
|
||||||
),
|
|
||||||
onPressed: () {
|
|
||||||
final activityName =
|
|
||||||
task.workItem?.activityMaster?.activityName ?? 'N/A';
|
|
||||||
final assigned = '${(task.plannedTask - completed)}';
|
|
||||||
final assignedBy =
|
|
||||||
"${task.assignedBy.firstName} ${task.assignedBy.lastName ?? ''}";
|
|
||||||
final assignedOn = DateFormat('dd-MM-yyyy').format(task.assignmentDate);
|
|
||||||
final taskId = task.id;
|
|
||||||
final location = [
|
|
||||||
task.workItem?.workArea?.floor?.building?.name,
|
|
||||||
task.workItem?.workArea?.floor?.floorName,
|
|
||||||
task.workItem?.workArea?.areaName,
|
|
||||||
].where((e) => e != null && e.isNotEmpty).join(' > ');
|
|
||||||
|
|
||||||
final teamMembers = task.teamMembers.map((e) => e.firstName).toList();
|
|
||||||
final pendingWork = (task.workItem?.plannedWork ?? 0) -
|
|
||||||
(task.workItem?.completedWork ?? 0);
|
|
||||||
|
|
||||||
final taskData = {
|
|
||||||
'activity': activityName,
|
|
||||||
'assigned': assigned,
|
|
||||||
'taskId': taskId,
|
|
||||||
'assignedBy': assignedBy,
|
|
||||||
'completed': completed,
|
|
||||||
'assignedOn': assignedOn,
|
|
||||||
'location': location,
|
|
||||||
'teamSize': task.teamMembers.length,
|
|
||||||
'teamMembers': teamMembers,
|
|
||||||
'pendingWork': pendingWork,
|
|
||||||
};
|
|
||||||
|
|
||||||
showModalBottomSheet(
|
|
||||||
context: context,
|
|
||||||
isScrollControlled: true,
|
|
||||||
shape: const RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
|
|
||||||
),
|
|
||||||
builder: (ctx) => Padding(
|
|
||||||
padding: MediaQuery.of(ctx).viewInsets,
|
|
||||||
child: ReportTaskBottomSheet(
|
|
||||||
taskData: taskData,
|
|
||||||
onReportSuccess: refreshCallback,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
static Widget commentButton({
|
|
||||||
required BuildContext context,
|
|
||||||
required dynamic task,
|
|
||||||
required VoidCallback refreshCallback,
|
|
||||||
required String parentTaskID,
|
|
||||||
required String activityId,
|
|
||||||
required String workAreaId,
|
|
||||||
}) {
|
|
||||||
return OutlinedButton.icon(
|
|
||||||
icon: const Icon(Icons.comment, size: 18, color: Colors.blueAccent),
|
|
||||||
label: const Text('Comment', style: TextStyle(color: Colors.blueAccent)),
|
|
||||||
style: OutlinedButton.styleFrom(
|
|
||||||
side: const BorderSide(color: Colors.blueAccent),
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
|
||||||
textStyle: const TextStyle(fontSize: 14),
|
|
||||||
),
|
|
||||||
onPressed: () {
|
|
||||||
final taskData =
|
|
||||||
_prepareTaskData(task: task, completed: task.completedTask.toInt());
|
|
||||||
|
|
||||||
showModalBottomSheet(
|
|
||||||
context: context,
|
|
||||||
isScrollControlled: true,
|
|
||||||
backgroundColor: Colors.transparent,
|
|
||||||
builder: (_) => CommentTaskBottomSheet(
|
|
||||||
taskData: taskData,
|
|
||||||
taskDataId: parentTaskID,
|
|
||||||
workAreaId: workAreaId,
|
|
||||||
activityId: activityId,
|
|
||||||
onCommentSuccess: () {
|
|
||||||
refreshCallback();
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
static Widget reportActionButton({
|
|
||||||
required BuildContext context,
|
|
||||||
required dynamic task,
|
|
||||||
required int completed,
|
|
||||||
required VoidCallback refreshCallback,
|
|
||||||
required String parentTaskID,
|
|
||||||
required String activityId,
|
|
||||||
required String workAreaId,
|
|
||||||
}) {
|
|
||||||
return OutlinedButton.icon(
|
|
||||||
icon: const Icon(Icons.report, size: 18, color: Colors.amber),
|
|
||||||
label: const Text('Take Report Action',
|
|
||||||
style: TextStyle(color: Colors.amber)),
|
|
||||||
style: OutlinedButton.styleFrom(
|
|
||||||
side: const BorderSide(color: Colors.amber),
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
|
||||||
textStyle: const TextStyle(fontSize: 14),
|
|
||||||
),
|
|
||||||
onPressed: () {
|
|
||||||
final taskData = _prepareTaskData(task: task, completed: completed);
|
|
||||||
|
|
||||||
showModalBottomSheet(
|
|
||||||
context: context,
|
|
||||||
isScrollControlled: true,
|
|
||||||
shape: const RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
|
|
||||||
),
|
|
||||||
builder: (ctx) => Padding(
|
|
||||||
padding: MediaQuery.of(ctx).viewInsets,
|
|
||||||
child: ReportActionBottomSheet(
|
|
||||||
taskData: taskData,
|
|
||||||
taskDataId: parentTaskID,
|
|
||||||
workAreaId: workAreaId,
|
|
||||||
activityId: activityId,
|
|
||||||
onReportSuccess: refreshCallback,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
static Map<String, dynamic> _prepareTaskData({
|
|
||||||
required dynamic task,
|
|
||||||
required int completed,
|
|
||||||
}) {
|
|
||||||
final activityName = task.workItem?.activityMaster?.activityName ?? 'N/A';
|
|
||||||
final assigned = '${(task.plannedTask - completed)}';
|
|
||||||
final assignedBy =
|
|
||||||
"${task.assignedBy.firstName} ${task.assignedBy.lastName ?? ''}";
|
|
||||||
final assignedOn = DateFormat('yyyy-MM-dd').format(task.assignmentDate);
|
|
||||||
final taskId = task.id;
|
|
||||||
|
|
||||||
final location = [
|
|
||||||
task.workItem?.workArea?.floor?.building?.name,
|
|
||||||
task.workItem?.workArea?.floor?.floorName,
|
|
||||||
task.workItem?.workArea?.areaName,
|
|
||||||
].where((e) => e != null && e.isNotEmpty).join(' > ');
|
|
||||||
|
|
||||||
final teamMembers = task.teamMembers
|
|
||||||
.map((e) => '${e.firstName} ${e.lastName ?? ''}')
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
final pendingWork =
|
|
||||||
(task.workItem?.plannedWork ?? 0) - (task.workItem?.completedWork ?? 0);
|
|
||||||
|
|
||||||
final taskComments = task.comments.map((comment) {
|
|
||||||
final isoDate = comment.timestamp.toIso8601String();
|
|
||||||
final commenterName = comment.commentedBy.firstName.isNotEmpty
|
|
||||||
? "${comment.commentedBy.firstName} ${comment.commentedBy.lastName ?? ''}"
|
|
||||||
.trim()
|
|
||||||
: "Unknown";
|
|
||||||
|
|
||||||
return {
|
|
||||||
'text': comment.comment,
|
|
||||||
'date': isoDate,
|
|
||||||
'commentedBy': commenterName,
|
|
||||||
'preSignedUrls': comment.preSignedUrls,
|
|
||||||
};
|
|
||||||
}).toList();
|
|
||||||
|
|
||||||
final taskLevelPreSignedUrls = task.reportedPreSignedUrls;
|
|
||||||
|
|
||||||
return {
|
|
||||||
'activity': activityName,
|
|
||||||
'assigned': assigned,
|
|
||||||
'taskId': taskId,
|
|
||||||
'assignedBy': assignedBy,
|
|
||||||
'completed': completed,
|
|
||||||
'plannedWork': task.plannedTask.toString(),
|
|
||||||
'completedWork': completed.toString(),
|
|
||||||
'assignedOn': assignedOn,
|
|
||||||
'location': location,
|
|
||||||
'teamSize': task.teamMembers.length,
|
|
||||||
'teamMembers': teamMembers,
|
|
||||||
'pendingWork': pendingWork,
|
|
||||||
'taskComments': taskComments,
|
|
||||||
'reportedPreSignedUrls': taskLevelPreSignedUrls,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,45 +0,0 @@
|
|||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
import 'package:marco/helpers/services/json_decoder.dart';
|
|
||||||
import 'package:marco/model/identifier_model.dart';
|
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
|
|
||||||
class TaskListModel extends IdentifierModel {
|
|
||||||
final String title, description, priority, status;
|
|
||||||
final DateTime dueDate;
|
|
||||||
late bool isSelectTask;
|
|
||||||
|
|
||||||
TaskListModel(super.id, this.title, this.description, this.priority, this.status, this.dueDate, this.isSelectTask);
|
|
||||||
|
|
||||||
static TaskListModel fromJSON(Map<String, dynamic> json) {
|
|
||||||
JSONDecoder decoder = JSONDecoder(json);
|
|
||||||
|
|
||||||
String title = decoder.getString('title');
|
|
||||||
String description = decoder.getString('description');
|
|
||||||
String priority = decoder.getString('priority');
|
|
||||||
String status = decoder.getString('status');
|
|
||||||
DateTime dueDate = decoder.getDateTime('due_date');
|
|
||||||
bool isSelectTask = decoder.getBool('isSelectTask');
|
|
||||||
|
|
||||||
return TaskListModel(decoder.getId, title, description, priority, status, dueDate, isSelectTask);
|
|
||||||
}
|
|
||||||
|
|
||||||
static List<TaskListModel> listFromJSON(List<dynamic> list) {
|
|
||||||
return list.map((e) => TaskListModel.fromJSON(e)).toList();
|
|
||||||
}
|
|
||||||
|
|
||||||
static List<TaskListModel>? _dummyList;
|
|
||||||
|
|
||||||
static Future<List<TaskListModel>> get dummyList async {
|
|
||||||
if (_dummyList == null) {
|
|
||||||
dynamic data = json.decode(await getData());
|
|
||||||
_dummyList = listFromJSON(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
return _dummyList!;
|
|
||||||
}
|
|
||||||
|
|
||||||
static Future<String> getData() async {
|
|
||||||
return await rootBundle.loadString('assets/data/task_list.json');
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,53 +0,0 @@
|
|||||||
class WorkStatusResponseModel {
|
|
||||||
final bool success;
|
|
||||||
final String message;
|
|
||||||
final List<WorkStatus> data;
|
|
||||||
final dynamic errors;
|
|
||||||
final int statusCode;
|
|
||||||
final DateTime timestamp;
|
|
||||||
|
|
||||||
WorkStatusResponseModel({
|
|
||||||
required this.success,
|
|
||||||
required this.message,
|
|
||||||
required this.data,
|
|
||||||
required this.errors,
|
|
||||||
required this.statusCode,
|
|
||||||
required this.timestamp,
|
|
||||||
});
|
|
||||||
|
|
||||||
factory WorkStatusResponseModel.fromJson(Map<String, dynamic> json) {
|
|
||||||
return WorkStatusResponseModel(
|
|
||||||
success: json['success'],
|
|
||||||
message: json['message'],
|
|
||||||
data: List<WorkStatus>.from(
|
|
||||||
json['data'].map((item) => WorkStatus.fromJson(item)),
|
|
||||||
),
|
|
||||||
errors: json['errors'],
|
|
||||||
statusCode: json['statusCode'],
|
|
||||||
timestamp: DateTime.parse(json['timestamp']),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class WorkStatus {
|
|
||||||
final String id;
|
|
||||||
final String name;
|
|
||||||
final String description;
|
|
||||||
final bool isSystem;
|
|
||||||
|
|
||||||
WorkStatus({
|
|
||||||
required this.id,
|
|
||||||
required this.name,
|
|
||||||
required this.description,
|
|
||||||
required this.isSystem,
|
|
||||||
});
|
|
||||||
|
|
||||||
factory WorkStatus.fromJson(Map<String, dynamic> json) {
|
|
||||||
return WorkStatus(
|
|
||||||
id: json['id'],
|
|
||||||
name: json['name'],
|
|
||||||
description: json['description'],
|
|
||||||
isSystem: json['isSystem'],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,109 +0,0 @@
|
|||||||
class Tenant {
|
|
||||||
final String id;
|
|
||||||
final String name;
|
|
||||||
final String email;
|
|
||||||
final String? domainName;
|
|
||||||
final String contactName;
|
|
||||||
final String contactNumber;
|
|
||||||
final String? logoImage;
|
|
||||||
final String? organizationSize;
|
|
||||||
final Industry? industry;
|
|
||||||
final TenantStatus? tenantStatus;
|
|
||||||
|
|
||||||
Tenant({
|
|
||||||
required this.id,
|
|
||||||
required this.name,
|
|
||||||
required this.email,
|
|
||||||
this.domainName,
|
|
||||||
required this.contactName,
|
|
||||||
required this.contactNumber,
|
|
||||||
this.logoImage,
|
|
||||||
this.organizationSize,
|
|
||||||
this.industry,
|
|
||||||
this.tenantStatus,
|
|
||||||
});
|
|
||||||
|
|
||||||
factory Tenant.fromJson(Map<String, dynamic> json) {
|
|
||||||
return Tenant(
|
|
||||||
id: json['id'] ?? '',
|
|
||||||
name: json['name'] ?? '',
|
|
||||||
email: json['email'] ?? '',
|
|
||||||
domainName: json['domainName'] as String?,
|
|
||||||
contactName: json['contactName'] ?? '',
|
|
||||||
contactNumber: json['contactNumber'] ?? '',
|
|
||||||
logoImage: json['logoImage'] is String ? json['logoImage'] : null,
|
|
||||||
organizationSize: json['organizationSize'] is String
|
|
||||||
? json['organizationSize']
|
|
||||||
: null,
|
|
||||||
industry: json['industry'] != null
|
|
||||||
? Industry.fromJson(json['industry'])
|
|
||||||
: null,
|
|
||||||
tenantStatus: json['tenantStatus'] != null
|
|
||||||
? TenantStatus.fromJson(json['tenantStatus'])
|
|
||||||
: null,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
|
||||||
return {
|
|
||||||
'id': id,
|
|
||||||
'name': name,
|
|
||||||
'email': email,
|
|
||||||
'domainName': domainName,
|
|
||||||
'contactName': contactName,
|
|
||||||
'contactNumber': contactNumber,
|
|
||||||
'logoImage': logoImage,
|
|
||||||
'organizationSize': organizationSize,
|
|
||||||
'industry': industry?.toJson(),
|
|
||||||
'tenantStatus': tenantStatus?.toJson(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class Industry {
|
|
||||||
final String id;
|
|
||||||
final String name;
|
|
||||||
|
|
||||||
Industry({
|
|
||||||
required this.id,
|
|
||||||
required this.name,
|
|
||||||
});
|
|
||||||
|
|
||||||
factory Industry.fromJson(Map<String, dynamic> json) {
|
|
||||||
return Industry(
|
|
||||||
id: json['id'] ?? '',
|
|
||||||
name: json['name'] ?? '',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
|
||||||
return {
|
|
||||||
'id': id,
|
|
||||||
'name': name,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class TenantStatus {
|
|
||||||
final String id;
|
|
||||||
final String name;
|
|
||||||
|
|
||||||
TenantStatus({
|
|
||||||
required this.id,
|
|
||||||
required this.name,
|
|
||||||
});
|
|
||||||
|
|
||||||
factory TenantStatus.fromJson(Map<String, dynamic> json) {
|
|
||||||
return TenantStatus(
|
|
||||||
id: json['id'] ?? '',
|
|
||||||
name: json['name'] ?? '',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
|
||||||
return {
|
|
||||||
'id': id,
|
|
||||||
'name': name,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,78 +0,0 @@
|
|||||||
class ServiceListResponse {
|
|
||||||
final bool success;
|
|
||||||
final String message;
|
|
||||||
final List<Service> data;
|
|
||||||
final dynamic errors;
|
|
||||||
final int statusCode;
|
|
||||||
final String timestamp;
|
|
||||||
|
|
||||||
ServiceListResponse({
|
|
||||||
required this.success,
|
|
||||||
required this.message,
|
|
||||||
required this.data,
|
|
||||||
this.errors,
|
|
||||||
required this.statusCode,
|
|
||||||
required this.timestamp,
|
|
||||||
});
|
|
||||||
|
|
||||||
factory ServiceListResponse.fromJson(Map<String, dynamic> json) {
|
|
||||||
return ServiceListResponse(
|
|
||||||
success: json['success'] ?? false,
|
|
||||||
message: json['message'] ?? '',
|
|
||||||
data: (json['data'] as List<dynamic>?)
|
|
||||||
?.map((e) => Service.fromJson(e))
|
|
||||||
.toList() ??
|
|
||||||
[],
|
|
||||||
errors: json['errors'],
|
|
||||||
statusCode: json['statusCode'] ?? 0,
|
|
||||||
timestamp: json['timestamp'] ?? '',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
|
||||||
return {
|
|
||||||
'success': success,
|
|
||||||
'message': message,
|
|
||||||
'data': data.map((e) => e.toJson()).toList(),
|
|
||||||
'errors': errors,
|
|
||||||
'statusCode': statusCode,
|
|
||||||
'timestamp': timestamp,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class Service {
|
|
||||||
final String id;
|
|
||||||
final String name;
|
|
||||||
final String description;
|
|
||||||
final bool isSystem;
|
|
||||||
final bool isActive;
|
|
||||||
|
|
||||||
Service({
|
|
||||||
required this.id,
|
|
||||||
required this.name,
|
|
||||||
required this.description,
|
|
||||||
required this.isSystem,
|
|
||||||
required this.isActive,
|
|
||||||
});
|
|
||||||
|
|
||||||
factory Service.fromJson(Map<String, dynamic> json) {
|
|
||||||
return Service(
|
|
||||||
id: json['id'] ?? '',
|
|
||||||
name: json['name'] ?? '',
|
|
||||||
description: json['description'] ?? '',
|
|
||||||
isSystem: json['isSystem'] ?? false,
|
|
||||||
isActive: json['isActive'] ?? false,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
|
||||||
return {
|
|
||||||
'id': id,
|
|
||||||
'name': name,
|
|
||||||
'description': description,
|
|
||||||
'isSystem': isSystem,
|
|
||||||
'isActive': isActive,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
@ -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/error_pages/error_500_screen.dart';
|
||||||
import 'package:marco/view/dashboard/dashboard_screen.dart';
|
import 'package:marco/view/dashboard/dashboard_screen.dart';
|
||||||
import 'package:marco/view/Attendence/attendance_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/employees/employees_screen.dart';
|
||||||
import 'package:marco/view/auth/login_option_screen.dart';
|
import 'package:marco/view/auth/login_option_screen.dart';
|
||||||
import 'package:marco/view/auth/mpin_screen.dart';
|
import 'package:marco/view/auth/mpin_screen.dart';
|
||||||
@ -55,15 +53,7 @@ getPageRoute() {
|
|||||||
name: '/dashboard/employees',
|
name: '/dashboard/employees',
|
||||||
page: () => EmployeesScreen(),
|
page: () => EmployeesScreen(),
|
||||||
middlewares: [AuthMiddleware()]),
|
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(
|
GetPage(
|
||||||
name: '/dashboard/directory-main-page',
|
name: '/dashboard/directory-main-page',
|
||||||
page: () => DirectoryMainScreen(),
|
page: () => DirectoryMainScreen(),
|
||||||
|
@ -1,572 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:get/get.dart';
|
|
||||||
import 'package:intl/intl.dart';
|
|
||||||
import 'package:marco/helpers/theme/app_theme.dart';
|
|
||||||
import 'package:marco/helpers/utils/mixins/ui_mixin.dart';
|
|
||||||
import 'package:marco/helpers/utils/my_shadow.dart';
|
|
||||||
import 'package:marco/helpers/widgets/my_card.dart';
|
|
||||||
import 'package:marco/helpers/widgets/my_container.dart';
|
|
||||||
import 'package:marco/helpers/widgets/my_spacing.dart';
|
|
||||||
import 'package:marco/helpers/widgets/my_text.dart';
|
|
||||||
import 'package:marco/controller/permission_controller.dart';
|
|
||||||
import 'package:marco/controller/task_planning/daily_task_controller.dart';
|
|
||||||
import 'package:marco/model/dailyTaskPlanning/daily_progress_report_filter.dart';
|
|
||||||
import 'package:marco/helpers/widgets/avatar.dart';
|
|
||||||
import 'package:marco/controller/project_controller.dart';
|
|
||||||
import 'package:marco/model/dailyTaskPlanning/task_action_buttons.dart';
|
|
||||||
import 'package:marco/helpers/widgets/my_custom_skeleton.dart';
|
|
||||||
import 'package:marco/helpers/utils/permission_constants.dart';
|
|
||||||
import 'package:marco/helpers/widgets/my_refresh_indicator.dart';
|
|
||||||
import 'package:marco/controller/tenant/service_controller.dart';
|
|
||||||
import 'package:marco/helpers/widgets/tenant/service_selector.dart';
|
|
||||||
|
|
||||||
class DailyProgressReportScreen extends StatefulWidget {
|
|
||||||
const DailyProgressReportScreen({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<DailyProgressReportScreen> createState() =>
|
|
||||||
_DailyProgressReportScreenState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class TaskChartData {
|
|
||||||
final String label;
|
|
||||||
final num value;
|
|
||||||
final Color color;
|
|
||||||
|
|
||||||
TaskChartData(this.label, this.value, this.color);
|
|
||||||
}
|
|
||||||
|
|
||||||
class _DailyProgressReportScreenState extends State<DailyProgressReportScreen>
|
|
||||||
with UIMixin {
|
|
||||||
final DailyTaskController dailyTaskController =
|
|
||||||
Get.put(DailyTaskController());
|
|
||||||
final PermissionController permissionController =
|
|
||||||
Get.find<PermissionController>();
|
|
||||||
final ProjectController projectController = Get.find<ProjectController>();
|
|
||||||
final ServiceController serviceController = Get.put(ServiceController());
|
|
||||||
final ScrollController _scrollController = ScrollController();
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
_scrollController.addListener(() {
|
|
||||||
if (_scrollController.position.pixels >=
|
|
||||||
_scrollController.position.maxScrollExtent - 100 &&
|
|
||||||
dailyTaskController.hasMore &&
|
|
||||||
!dailyTaskController.isLoadingMore.value) {
|
|
||||||
final projectId = dailyTaskController.selectedProjectId;
|
|
||||||
if (projectId != null && projectId.isNotEmpty) {
|
|
||||||
dailyTaskController.fetchTaskData(
|
|
||||||
projectId,
|
|
||||||
pageNumber: dailyTaskController.currentPage + 1,
|
|
||||||
pageSize: dailyTaskController.pageSize,
|
|
||||||
isLoadMore: true,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
final initialProjectId = projectController.selectedProjectId.value;
|
|
||||||
if (initialProjectId.isNotEmpty) {
|
|
||||||
dailyTaskController.selectedProjectId = initialProjectId;
|
|
||||||
dailyTaskController.fetchTaskData(initialProjectId);
|
|
||||||
serviceController.fetchServices(initialProjectId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update when project changes
|
|
||||||
ever<String>(projectController.selectedProjectId, (newProjectId) async {
|
|
||||||
if (newProjectId.isNotEmpty &&
|
|
||||||
newProjectId != dailyTaskController.selectedProjectId) {
|
|
||||||
dailyTaskController.selectedProjectId = newProjectId;
|
|
||||||
await dailyTaskController.fetchTaskData(newProjectId);
|
|
||||||
await serviceController.fetchServices(newProjectId);
|
|
||||||
dailyTaskController.update(['daily_progress_report_controller']);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_scrollController.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Scaffold(
|
|
||||||
appBar: PreferredSize(
|
|
||||||
preferredSize: const Size.fromHeight(72),
|
|
||||||
child: AppBar(
|
|
||||||
backgroundColor: const Color(0xFFF5F5F5),
|
|
||||||
elevation: 0.5,
|
|
||||||
automaticallyImplyLeading: false,
|
|
||||||
titleSpacing: 0,
|
|
||||||
title: Padding(
|
|
||||||
padding: MySpacing.xy(16, 0),
|
|
||||||
child: Row(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(Icons.arrow_back_ios_new,
|
|
||||||
color: Colors.black, size: 20),
|
|
||||||
onPressed: () => Get.offNamed('/dashboard'),
|
|
||||||
),
|
|
||||||
MySpacing.width(8),
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
MyText.titleLarge(
|
|
||||||
'Daily Progress Report',
|
|
||||||
fontWeight: 700,
|
|
||||||
color: Colors.black,
|
|
||||||
),
|
|
||||||
MySpacing.height(2),
|
|
||||||
GetBuilder<ProjectController>(
|
|
||||||
builder: (projectController) {
|
|
||||||
final projectName =
|
|
||||||
projectController.selectedProject?.name ??
|
|
||||||
'Select Project';
|
|
||||||
return Row(
|
|
||||||
children: [
|
|
||||||
const Icon(Icons.work_outline,
|
|
||||||
size: 14, color: Colors.grey),
|
|
||||||
MySpacing.width(4),
|
|
||||||
Expanded(
|
|
||||||
child: MyText.bodySmall(
|
|
||||||
projectName,
|
|
||||||
fontWeight: 600,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
color: Colors.grey[700],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
body: SafeArea(
|
|
||||||
child: MyRefreshIndicator(
|
|
||||||
onRefresh: _refreshData,
|
|
||||||
child: CustomScrollView(
|
|
||||||
physics: const AlwaysScrollableScrollPhysics(),
|
|
||||||
slivers: [
|
|
||||||
SliverToBoxAdapter(
|
|
||||||
child: GetBuilder<DailyTaskController>(
|
|
||||||
init: dailyTaskController,
|
|
||||||
tag: 'daily_progress_report_controller',
|
|
||||||
builder: (controller) {
|
|
||||||
return Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
MySpacing.height(flexSpacing),
|
|
||||||
|
|
||||||
// --- ADD SERVICE SELECTOR HERE ---
|
|
||||||
Padding(
|
|
||||||
padding: MySpacing.x(10),
|
|
||||||
child: ServiceSelector(
|
|
||||||
controller: serviceController,
|
|
||||||
height: 40,
|
|
||||||
onSelectionChanged: (service) async {
|
|
||||||
final projectId =
|
|
||||||
dailyTaskController.selectedProjectId;
|
|
||||||
if (projectId?.isNotEmpty ?? false) {
|
|
||||||
await dailyTaskController.fetchTaskData(
|
|
||||||
projectId!,
|
|
||||||
serviceIds:
|
|
||||||
service != null ? [service.id] : null,
|
|
||||||
pageNumber: 1,
|
|
||||||
pageSize: 20,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
_buildActionBar(),
|
|
||||||
Padding(
|
|
||||||
padding: MySpacing.x(8),
|
|
||||||
child: _buildDailyProgressReportTab(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildActionBar() {
|
|
||||||
return Padding(
|
|
||||||
padding: MySpacing.x(flexSpacing),
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
|
||||||
children: [
|
|
||||||
_buildActionItem(
|
|
||||||
label: "Filter",
|
|
||||||
icon: Icons.tune,
|
|
||||||
tooltip: 'Filter Project',
|
|
||||||
onTap: _openFilterSheet,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildActionItem({
|
|
||||||
required String label,
|
|
||||||
required IconData icon,
|
|
||||||
required String tooltip,
|
|
||||||
required VoidCallback onTap,
|
|
||||||
Color? color,
|
|
||||||
}) {
|
|
||||||
return Row(
|
|
||||||
children: [
|
|
||||||
MyText.bodyMedium(label, fontWeight: 600),
|
|
||||||
Tooltip(
|
|
||||||
message: tooltip,
|
|
||||||
child: InkWell(
|
|
||||||
borderRadius: BorderRadius.circular(22),
|
|
||||||
onTap: onTap,
|
|
||||||
child: MouseRegion(
|
|
||||||
cursor: SystemMouseCursors.click,
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(8.0),
|
|
||||||
child: Icon(icon, color: color, size: 22),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _openFilterSheet() async {
|
|
||||||
final result = await showModalBottomSheet<Map<String, dynamic>>(
|
|
||||||
context: context,
|
|
||||||
isScrollControlled: true,
|
|
||||||
backgroundColor: Colors.transparent,
|
|
||||||
builder: (context) => DailyProgressReportFilter(
|
|
||||||
controller: dailyTaskController,
|
|
||||||
permissionController: permissionController,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (result != null) {
|
|
||||||
final selectedProjectId = result['projectId'] as String?;
|
|
||||||
if (selectedProjectId != null &&
|
|
||||||
selectedProjectId != dailyTaskController.selectedProjectId) {
|
|
||||||
dailyTaskController.selectedProjectId = selectedProjectId;
|
|
||||||
await dailyTaskController.fetchTaskData(selectedProjectId);
|
|
||||||
dailyTaskController.update(['daily_progress_report_controller']);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _refreshData() async {
|
|
||||||
final projectId = dailyTaskController.selectedProjectId;
|
|
||||||
if (projectId != null) {
|
|
||||||
try {
|
|
||||||
await dailyTaskController.fetchTaskData(projectId);
|
|
||||||
} catch (e) {
|
|
||||||
debugPrint('Error refreshing task data: $e');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _showTeamMembersBottomSheet(List<dynamic> members) {
|
|
||||||
showModalBottomSheet(
|
|
||||||
context: context,
|
|
||||||
isScrollControlled: true,
|
|
||||||
backgroundColor: Colors.transparent,
|
|
||||||
isDismissible: true,
|
|
||||||
enableDrag: true,
|
|
||||||
shape: const RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.vertical(top: Radius.circular(12)),
|
|
||||||
),
|
|
||||||
builder: (context) {
|
|
||||||
return GestureDetector(
|
|
||||||
onTap: () {},
|
|
||||||
child: Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.white,
|
|
||||||
borderRadius:
|
|
||||||
const BorderRadius.vertical(top: Radius.circular(12)),
|
|
||||||
),
|
|
||||||
padding: const EdgeInsets.fromLTRB(16, 24, 16, 24),
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
MyText.titleMedium(
|
|
||||||
'Team Members',
|
|
||||||
fontWeight: 600,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
const Divider(thickness: 1),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
...members.map((member) {
|
|
||||||
final firstName = member.firstName ?? 'Unnamed';
|
|
||||||
final lastName = member.lastName ?? 'User';
|
|
||||||
return ListTile(
|
|
||||||
contentPadding: EdgeInsets.zero,
|
|
||||||
leading: Avatar(
|
|
||||||
firstName: firstName,
|
|
||||||
lastName: lastName,
|
|
||||||
size: 31,
|
|
||||||
),
|
|
||||||
title: MyText.bodyMedium(
|
|
||||||
'$firstName $lastName',
|
|
||||||
fontWeight: 600,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildDailyProgressReportTab() {
|
|
||||||
return Obx(() {
|
|
||||||
final isLoading = dailyTaskController.isLoading.value;
|
|
||||||
final groupedTasks = dailyTaskController.groupedDailyTasks;
|
|
||||||
|
|
||||||
// Initial loading skeleton
|
|
||||||
if (isLoading && dailyTaskController.currentPage == 1) {
|
|
||||||
return SkeletonLoaders.dailyProgressReportSkeletonLoader();
|
|
||||||
}
|
|
||||||
|
|
||||||
// No tasks
|
|
||||||
if (groupedTasks.isEmpty) {
|
|
||||||
return Center(
|
|
||||||
child: MyText.bodySmall(
|
|
||||||
"No Progress Report Found",
|
|
||||||
fontWeight: 600,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
final sortedDates = groupedTasks.keys.toList()
|
|
||||||
..sort((a, b) => b.compareTo(a));
|
|
||||||
|
|
||||||
// If only one date, make it expanded by default
|
|
||||||
if (sortedDates.length == 1 &&
|
|
||||||
!dailyTaskController.expandedDates.contains(sortedDates[0])) {
|
|
||||||
dailyTaskController.expandedDates.add(sortedDates[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return MyCard.bordered(
|
|
||||||
borderRadiusAll: 10,
|
|
||||||
border: Border.all(color: Colors.grey.withOpacity(0.2)),
|
|
||||||
shadow: MyShadow(elevation: 1, position: MyShadowPosition.bottom),
|
|
||||||
paddingAll: 8,
|
|
||||||
child: ListView.builder(
|
|
||||||
controller: _scrollController,
|
|
||||||
shrinkWrap: true,
|
|
||||||
physics: const AlwaysScrollableScrollPhysics(),
|
|
||||||
itemCount: sortedDates.length + 1, // +1 for loading indicator
|
|
||||||
itemBuilder: (context, dateIndex) {
|
|
||||||
// Bottom loading indicator
|
|
||||||
if (dateIndex == sortedDates.length) {
|
|
||||||
return Obx(() => dailyTaskController.isLoadingMore.value
|
|
||||||
? const Padding(
|
|
||||||
padding: EdgeInsets.all(16.0),
|
|
||||||
child: Center(child: CircularProgressIndicator()),
|
|
||||||
)
|
|
||||||
: const SizedBox.shrink());
|
|
||||||
}
|
|
||||||
|
|
||||||
final dateKey = sortedDates[dateIndex];
|
|
||||||
final tasksForDate = groupedTasks[dateKey]!;
|
|
||||||
final date = DateTime.tryParse(dateKey);
|
|
||||||
|
|
||||||
return Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
GestureDetector(
|
|
||||||
onTap: () => dailyTaskController.toggleDate(dateKey),
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
MyText.bodyMedium(
|
|
||||||
date != null
|
|
||||||
? DateFormat('dd MMM yyyy').format(date)
|
|
||||||
: dateKey,
|
|
||||||
fontWeight: 700,
|
|
||||||
),
|
|
||||||
Obx(() => Icon(
|
|
||||||
dailyTaskController.expandedDates.contains(dateKey)
|
|
||||||
? Icons.remove_circle
|
|
||||||
: Icons.add_circle,
|
|
||||||
color: Colors.blueAccent,
|
|
||||||
)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Obx(() {
|
|
||||||
if (!dailyTaskController.expandedDates.contains(dateKey)) {
|
|
||||||
return const SizedBox.shrink();
|
|
||||||
}
|
|
||||||
|
|
||||||
return Column(
|
|
||||||
children: tasksForDate.asMap().entries.map((entry) {
|
|
||||||
final task = entry.value;
|
|
||||||
|
|
||||||
final activityName =
|
|
||||||
task.workItem?.activityMaster?.activityName ?? 'N/A';
|
|
||||||
final activityId = task.workItem?.activityMaster?.id;
|
|
||||||
final workAreaId = task.workItem?.workArea?.id;
|
|
||||||
final location = [
|
|
||||||
task.workItem?.workArea?.floor?.building?.name,
|
|
||||||
task.workItem?.workArea?.floor?.floorName,
|
|
||||||
task.workItem?.workArea?.areaName
|
|
||||||
].where((e) => e?.isNotEmpty ?? false).join(' > ');
|
|
||||||
|
|
||||||
final planned = task.plannedTask;
|
|
||||||
final completed = task.completedTask;
|
|
||||||
final progress = (planned != 0)
|
|
||||||
? (completed / planned).clamp(0.0, 1.0)
|
|
||||||
: 0.0;
|
|
||||||
final parentTaskID = task.id;
|
|
||||||
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.only(bottom: 8),
|
|
||||||
child: MyContainer(
|
|
||||||
paddingAll: 12,
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
MyText.bodyMedium(activityName, fontWeight: 600),
|
|
||||||
const SizedBox(height: 2),
|
|
||||||
MyText.bodySmall(location, color: Colors.grey),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
GestureDetector(
|
|
||||||
onTap: () => _showTeamMembersBottomSheet(
|
|
||||||
task.teamMembers),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
const Icon(Icons.group,
|
|
||||||
size: 18, color: Colors.blueAccent),
|
|
||||||
const SizedBox(width: 6),
|
|
||||||
MyText.bodyMedium('Team',
|
|
||||||
color: Colors.blueAccent,
|
|
||||||
fontWeight: 600),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
MyText.bodySmall(
|
|
||||||
"Completed: $completed / $planned",
|
|
||||||
fontWeight: 600,
|
|
||||||
color: Colors.black87,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 6),
|
|
||||||
Stack(
|
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
height: 5,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.grey[300],
|
|
||||||
borderRadius: BorderRadius.circular(6),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
FractionallySizedBox(
|
|
||||||
widthFactor: progress,
|
|
||||||
child: Container(
|
|
||||||
height: 5,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: progress >= 1.0
|
|
||||||
? Colors.green
|
|
||||||
: progress >= 0.5
|
|
||||||
? Colors.amber
|
|
||||||
: Colors.red,
|
|
||||||
borderRadius: BorderRadius.circular(6),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 4),
|
|
||||||
MyText.bodySmall(
|
|
||||||
"${(progress * 100).toStringAsFixed(1)}%",
|
|
||||||
fontWeight: 500,
|
|
||||||
color: progress >= 1.0
|
|
||||||
? Colors.green[700]
|
|
||||||
: progress >= 0.5
|
|
||||||
? Colors.amber[800]
|
|
||||||
: Colors.red[700],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
SingleChildScrollView(
|
|
||||||
scrollDirection: Axis.horizontal,
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
|
||||||
children: [
|
|
||||||
if ((task.reportedDate == null ||
|
|
||||||
task.reportedDate
|
|
||||||
.toString()
|
|
||||||
.isEmpty) &&
|
|
||||||
permissionController.hasPermission(
|
|
||||||
Permissions.assignReportTask)) ...[
|
|
||||||
TaskActionButtons.reportButton(
|
|
||||||
context: context,
|
|
||||||
task: task,
|
|
||||||
completed: completed.toInt(),
|
|
||||||
refreshCallback: _refreshData,
|
|
||||||
),
|
|
||||||
const SizedBox(width: 4),
|
|
||||||
] else if (task.approvedBy == null &&
|
|
||||||
permissionController.hasPermission(
|
|
||||||
Permissions.approveTask)) ...[
|
|
||||||
TaskActionButtons.reportActionButton(
|
|
||||||
context: context,
|
|
||||||
task: task,
|
|
||||||
parentTaskID: parentTaskID,
|
|
||||||
workAreaId: workAreaId.toString(),
|
|
||||||
activityId: activityId.toString(),
|
|
||||||
completed: completed.toInt(),
|
|
||||||
refreshCallback: _refreshData,
|
|
||||||
),
|
|
||||||
const SizedBox(width: 5),
|
|
||||||
],
|
|
||||||
TaskActionButtons.commentButton(
|
|
||||||
context: context,
|
|
||||||
task: task,
|
|
||||||
parentTaskID: parentTaskID,
|
|
||||||
workAreaId: workAreaId.toString(),
|
|
||||||
activityId: activityId.toString(),
|
|
||||||
refreshCallback: _refreshData,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}).toList(),
|
|
||||||
);
|
|
||||||
})
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,531 +0,0 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:get/get.dart';
|
|
||||||
import 'package:marco/helpers/theme/app_theme.dart';
|
|
||||||
import 'package:marco/helpers/utils/mixins/ui_mixin.dart';
|
|
||||||
import 'package:marco/helpers/widgets/my_card.dart';
|
|
||||||
import 'package:marco/helpers/widgets/my_spacing.dart';
|
|
||||||
import 'package:marco/helpers/widgets/my_text.dart';
|
|
||||||
import 'package:marco/controller/permission_controller.dart';
|
|
||||||
import 'package:marco/controller/task_Planning/daily_task_Planning_controller.dart';
|
|
||||||
import 'package:marco/controller/project_controller.dart';
|
|
||||||
import 'package:percent_indicator/percent_indicator.dart';
|
|
||||||
import 'package:marco/model/dailyTaskPlanning/assign_task_bottom_sheet .dart';
|
|
||||||
import 'package:marco/helpers/widgets/my_custom_skeleton.dart';
|
|
||||||
import 'package:marco/helpers/utils/permission_constants.dart';
|
|
||||||
import 'package:marco/helpers/widgets/my_refresh_indicator.dart';
|
|
||||||
import 'package:marco/controller/tenant/service_controller.dart';
|
|
||||||
import 'package:marco/helpers/widgets/tenant/service_selector.dart';
|
|
||||||
|
|
||||||
class DailyTaskPlanningScreen extends StatefulWidget {
|
|
||||||
DailyTaskPlanningScreen({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<DailyTaskPlanningScreen> createState() =>
|
|
||||||
_DailyTaskPlanningScreenState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _DailyTaskPlanningScreenState extends State<DailyTaskPlanningScreen>
|
|
||||||
with UIMixin {
|
|
||||||
final DailyTaskPlanningController dailyTaskPlanningController =
|
|
||||||
Get.put(DailyTaskPlanningController());
|
|
||||||
final PermissionController permissionController =
|
|
||||||
Get.put(PermissionController());
|
|
||||||
final ProjectController projectController = Get.find<ProjectController>();
|
|
||||||
final ServiceController serviceController = Get.put(ServiceController());
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
|
|
||||||
final projectId = projectController.selectedProjectId.value;
|
|
||||||
if (projectId.isNotEmpty) {
|
|
||||||
dailyTaskPlanningController.fetchTaskData(projectId);
|
|
||||||
serviceController.fetchServices(projectId); // <-- Fetch services here
|
|
||||||
}
|
|
||||||
|
|
||||||
ever<String>(
|
|
||||||
projectController.selectedProjectId,
|
|
||||||
(newProjectId) {
|
|
||||||
if (newProjectId.isNotEmpty) {
|
|
||||||
dailyTaskPlanningController.fetchTaskData(newProjectId);
|
|
||||||
serviceController
|
|
||||||
.fetchServices(newProjectId);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Scaffold(
|
|
||||||
appBar: PreferredSize(
|
|
||||||
preferredSize: const Size.fromHeight(72),
|
|
||||||
child: AppBar(
|
|
||||||
backgroundColor: const Color(0xFFF5F5F5),
|
|
||||||
elevation: 0.5,
|
|
||||||
automaticallyImplyLeading: false,
|
|
||||||
titleSpacing: 0,
|
|
||||||
title: Padding(
|
|
||||||
padding: MySpacing.xy(16, 0),
|
|
||||||
child: Row(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(Icons.arrow_back_ios_new,
|
|
||||||
color: Colors.black, size: 20),
|
|
||||||
onPressed: () => Get.offNamed('/dashboard'),
|
|
||||||
),
|
|
||||||
MySpacing.width(8),
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
MyText.titleLarge(
|
|
||||||
'Daily Task Planning',
|
|
||||||
fontWeight: 700,
|
|
||||||
color: Colors.black,
|
|
||||||
),
|
|
||||||
MySpacing.height(2),
|
|
||||||
GetBuilder<ProjectController>(
|
|
||||||
builder: (projectController) {
|
|
||||||
final projectName =
|
|
||||||
projectController.selectedProject?.name ??
|
|
||||||
'Select Project';
|
|
||||||
return Row(
|
|
||||||
children: [
|
|
||||||
const Icon(Icons.work_outline,
|
|
||||||
size: 14, color: Colors.grey),
|
|
||||||
MySpacing.width(4),
|
|
||||||
Expanded(
|
|
||||||
child: MyText.bodySmall(
|
|
||||||
projectName,
|
|
||||||
fontWeight: 600,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
color: Colors.grey[700],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
body: SafeArea(
|
|
||||||
child: MyRefreshIndicator(
|
|
||||||
onRefresh: () async {
|
|
||||||
final projectId = projectController.selectedProjectId.value;
|
|
||||||
if (projectId.isNotEmpty) {
|
|
||||||
try {
|
|
||||||
await dailyTaskPlanningController.fetchTaskData(projectId);
|
|
||||||
} catch (e) {
|
|
||||||
debugPrint('Error refreshing task data: ${e.toString()}');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
child: SingleChildScrollView(
|
|
||||||
physics:
|
|
||||||
const AlwaysScrollableScrollPhysics(), // <-- always allow drag
|
|
||||||
padding: MySpacing.x(0),
|
|
||||||
child: ConstrainedBox(
|
|
||||||
// <-- ensures full screen height
|
|
||||||
constraints: BoxConstraints(
|
|
||||||
minHeight: MediaQuery.of(context).size.height -
|
|
||||||
kToolbarHeight -
|
|
||||||
MediaQuery.of(context).padding.top,
|
|
||||||
),
|
|
||||||
child: GetBuilder<DailyTaskPlanningController>(
|
|
||||||
init: dailyTaskPlanningController,
|
|
||||||
tag: 'daily_task_Planning_controller',
|
|
||||||
builder: (controller) {
|
|
||||||
return Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
MySpacing.height(flexSpacing),
|
|
||||||
Padding(
|
|
||||||
padding: MySpacing.x(10),
|
|
||||||
child: ServiceSelector(
|
|
||||||
controller: serviceController,
|
|
||||||
height: 40,
|
|
||||||
onSelectionChanged: (service) async {
|
|
||||||
final projectId =
|
|
||||||
projectController.selectedProjectId.value;
|
|
||||||
if (projectId.isNotEmpty) {
|
|
||||||
await dailyTaskPlanningController.fetchTaskData(
|
|
||||||
projectId,
|
|
||||||
// serviceId: service
|
|
||||||
// ?.id,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
MySpacing.height(flexSpacing),
|
|
||||||
Padding(
|
|
||||||
padding: MySpacing.x(8),
|
|
||||||
child: dailyProgressReportTab(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget dailyProgressReportTab() {
|
|
||||||
return Obx(() {
|
|
||||||
final isLoading = dailyTaskPlanningController.isLoading.value;
|
|
||||||
final dailyTasks = dailyTaskPlanningController.dailyTasks;
|
|
||||||
|
|
||||||
if (isLoading) {
|
|
||||||
return SkeletonLoaders.dailyProgressPlanningSkeletonCollapsedOnly();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dailyTasks.isEmpty) {
|
|
||||||
return Center(
|
|
||||||
child: MyText.bodySmall(
|
|
||||||
"No Progress Report Found",
|
|
||||||
fontWeight: 600,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
final buildingExpansionState = <String, bool>{};
|
|
||||||
final floorExpansionState = <String, bool>{};
|
|
||||||
|
|
||||||
Widget buildExpandIcon(bool isExpanded) {
|
|
||||||
return Container(
|
|
||||||
width: 32,
|
|
||||||
height: 32,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
color: Colors.grey.shade200,
|
|
||||||
),
|
|
||||||
child: Icon(
|
|
||||||
isExpanded ? Icons.remove : Icons.add,
|
|
||||||
size: 20,
|
|
||||||
color: Colors.black87,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return StatefulBuilder(builder: (context, setMainState) {
|
|
||||||
final filteredBuildings = dailyTasks.expand((task) {
|
|
||||||
return task.buildings.where((building) {
|
|
||||||
return building.floors.any((floor) =>
|
|
||||||
floor.workAreas.any((area) => area.workItems.isNotEmpty));
|
|
||||||
});
|
|
||||||
}).toList();
|
|
||||||
|
|
||||||
if (filteredBuildings.isEmpty) {
|
|
||||||
return Center(
|
|
||||||
child: MyText.bodySmall(
|
|
||||||
"No Progress Report Found",
|
|
||||||
fontWeight: 600,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: filteredBuildings.map((building) {
|
|
||||||
final buildingKey = building.id.toString();
|
|
||||||
|
|
||||||
return MyCard.bordered(
|
|
||||||
borderRadiusAll: 10,
|
|
||||||
paddingAll: 0,
|
|
||||||
margin: MySpacing.bottom(10),
|
|
||||||
child: Theme(
|
|
||||||
data: Theme.of(context)
|
|
||||||
.copyWith(dividerColor: Colors.transparent),
|
|
||||||
child: ExpansionTile(
|
|
||||||
onExpansionChanged: (expanded) {
|
|
||||||
setMainState(() {
|
|
||||||
buildingExpansionState[buildingKey] = expanded;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
trailing: buildExpandIcon(
|
|
||||||
buildingExpansionState[buildingKey] ?? false),
|
|
||||||
tilePadding:
|
|
||||||
const EdgeInsets.symmetric(horizontal: 16, vertical: 0),
|
|
||||||
collapsedShape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
leading: Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.blueAccent.withOpacity(0.1),
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
),
|
|
||||||
padding: const EdgeInsets.all(8),
|
|
||||||
child: Icon(
|
|
||||||
Icons.location_city_rounded,
|
|
||||||
color: Colors.blueAccent,
|
|
||||||
size: 24,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
title: MyText.titleMedium(
|
|
||||||
building.name,
|
|
||||||
fontWeight: 700,
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
childrenPadding:
|
|
||||||
const EdgeInsets.symmetric(horizontal: 16, vertical: 0),
|
|
||||||
children: building.floors.expand((floor) {
|
|
||||||
final validWorkAreas = floor.workAreas
|
|
||||||
.where((area) => area.workItems.isNotEmpty);
|
|
||||||
|
|
||||||
// For each valid work area, return a Floor+WorkArea ExpansionTile
|
|
||||||
return validWorkAreas.map((area) {
|
|
||||||
final floorWorkAreaKey =
|
|
||||||
"${buildingKey}_${floor.floorName}_${area.areaName}";
|
|
||||||
final isExpanded =
|
|
||||||
floorExpansionState[floorWorkAreaKey] ?? false;
|
|
||||||
final workItems = area.workItems;
|
|
||||||
final totalPlanned = workItems.fold<double>(
|
|
||||||
0, (sum, wi) => sum + (wi.workItem.plannedWork ?? 0));
|
|
||||||
final totalCompleted = workItems.fold<double>(0,
|
|
||||||
(sum, wi) => sum + (wi.workItem.completedWork ?? 0));
|
|
||||||
final totalProgress = totalPlanned == 0
|
|
||||||
? 0.0
|
|
||||||
: (totalCompleted / totalPlanned).clamp(0.0, 1.0);
|
|
||||||
return ExpansionTile(
|
|
||||||
onExpansionChanged: (expanded) {
|
|
||||||
setMainState(() {
|
|
||||||
floorExpansionState[floorWorkAreaKey] = expanded;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
trailing: Icon(
|
|
||||||
isExpanded
|
|
||||||
? Icons.keyboard_arrow_up
|
|
||||||
: Icons.keyboard_arrow_down,
|
|
||||||
size: 28,
|
|
||||||
color: Colors.black54,
|
|
||||||
),
|
|
||||||
tilePadding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 16, vertical: 0),
|
|
||||||
title: Row(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
flex: 3,
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
MyText.titleSmall(
|
|
||||||
"Floor: ${floor.floorName}",
|
|
||||||
fontWeight: 600,
|
|
||||||
color: Colors.teal,
|
|
||||||
maxLines: null,
|
|
||||||
overflow: TextOverflow.visible,
|
|
||||||
softWrap: true,
|
|
||||||
),
|
|
||||||
MySpacing.height(4),
|
|
||||||
MyText.titleSmall(
|
|
||||||
"Work Area: ${area.areaName}",
|
|
||||||
fontWeight: 600,
|
|
||||||
color: Colors.blueGrey,
|
|
||||||
maxLines: null,
|
|
||||||
overflow: TextOverflow.visible,
|
|
||||||
softWrap: true,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
MySpacing.width(12),
|
|
||||||
CircularPercentIndicator(
|
|
||||||
radius: 20.0,
|
|
||||||
lineWidth: 4.0,
|
|
||||||
animation: true,
|
|
||||||
percent: totalProgress,
|
|
||||||
center: Text(
|
|
||||||
"${(totalProgress * 100).toStringAsFixed(0)}%",
|
|
||||||
style: TextStyle(
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
fontSize: 10.0,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
circularStrokeCap: CircularStrokeCap.round,
|
|
||||||
progressColor: totalProgress >= 1.0
|
|
||||||
? Colors.green
|
|
||||||
: (totalProgress >= 0.5
|
|
||||||
? Colors.amber
|
|
||||||
: Colors.red),
|
|
||||||
backgroundColor: Colors.grey[300]!,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
childrenPadding: const EdgeInsets.only(
|
|
||||||
left: 16, right: 0, bottom: 8),
|
|
||||||
children: area.workItems.map((wItem) {
|
|
||||||
final item = wItem.workItem;
|
|
||||||
final completed = item.completedWork ?? 0;
|
|
||||||
final planned = item.plannedWork ?? 0;
|
|
||||||
final progress = (planned == 0)
|
|
||||||
? 0.0
|
|
||||||
: (completed / planned).clamp(0.0, 1.0);
|
|
||||||
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: MyText.bodyMedium(
|
|
||||||
item.activityMaster?.name ??
|
|
||||||
"No Activity",
|
|
||||||
fontWeight: 600,
|
|
||||||
maxLines: 2,
|
|
||||||
overflow: TextOverflow.visible,
|
|
||||||
softWrap: true,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
MySpacing.width(8),
|
|
||||||
if (item.workCategoryMaster?.name != null)
|
|
||||||
Container(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 12, vertical: 4),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.blue.shade100,
|
|
||||||
borderRadius:
|
|
||||||
BorderRadius.circular(20),
|
|
||||||
),
|
|
||||||
child: MyText.bodySmall(
|
|
||||||
item.workCategoryMaster!.name!,
|
|
||||||
fontWeight: 500,
|
|
||||||
color: Colors.blue.shade800,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
MySpacing.height(4),
|
|
||||||
Row(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
flex: 3,
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment:
|
|
||||||
CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
MySpacing.height(8),
|
|
||||||
MyText.bodySmall(
|
|
||||||
"Completed: $completed / $planned",
|
|
||||||
fontWeight: 600,
|
|
||||||
color: const Color.fromARGB(
|
|
||||||
221, 0, 0, 0),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
MySpacing.width(16),
|
|
||||||
if (progress < 1.0 &&
|
|
||||||
permissionController.hasPermission(
|
|
||||||
Permissions.assignReportTask))
|
|
||||||
IconButton(
|
|
||||||
icon: Icon(
|
|
||||||
Icons.person_add_alt_1_rounded,
|
|
||||||
color:
|
|
||||||
Color.fromARGB(255, 46, 161, 233),
|
|
||||||
),
|
|
||||||
onPressed: () {
|
|
||||||
final pendingTask =
|
|
||||||
(planned - completed)
|
|
||||||
.clamp(0, planned)
|
|
||||||
.toInt();
|
|
||||||
|
|
||||||
showModalBottomSheet(
|
|
||||||
context: context,
|
|
||||||
isScrollControlled: true,
|
|
||||||
shape: const RoundedRectangleBorder(
|
|
||||||
borderRadius:
|
|
||||||
BorderRadius.vertical(
|
|
||||||
top: Radius.circular(16)),
|
|
||||||
),
|
|
||||||
builder: (context) =>
|
|
||||||
AssignTaskBottomSheet(
|
|
||||||
buildingName: building.name,
|
|
||||||
floorName: floor.floorName,
|
|
||||||
workAreaName: area.areaName,
|
|
||||||
workLocation: area.areaName,
|
|
||||||
activityName:
|
|
||||||
item.activityMaster?.name ??
|
|
||||||
"Unknown Activity",
|
|
||||||
pendingTask: pendingTask,
|
|
||||||
workItemId: item.id.toString(),
|
|
||||||
assignmentDate: DateTime.now(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
MySpacing.height(8),
|
|
||||||
Stack(
|
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
height: 5,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.grey[300],
|
|
||||||
borderRadius: BorderRadius.circular(6),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
FractionallySizedBox(
|
|
||||||
widthFactor: progress,
|
|
||||||
child: Container(
|
|
||||||
height: 5,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: progress >= 1.0
|
|
||||||
? Colors.green
|
|
||||||
: (progress >= 0.5
|
|
||||||
? Colors.amber
|
|
||||||
: Colors.red),
|
|
||||||
borderRadius:
|
|
||||||
BorderRadius.circular(6),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
SizedBox(height: 4),
|
|
||||||
MyText.bodySmall(
|
|
||||||
"${(progress * 100).toStringAsFixed(1)}%",
|
|
||||||
fontWeight: 500,
|
|
||||||
color: progress >= 1.0
|
|
||||||
? Colors.green[700]
|
|
||||||
: (progress >= 0.5
|
|
||||||
? Colors.amber[800]
|
|
||||||
: Colors.red[700]),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}).toList(),
|
|
||||||
);
|
|
||||||
}).toList();
|
|
||||||
}).toList(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}).toList(),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,391 +0,0 @@
|
|||||||
import 'dart:convert';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:get/get.dart';
|
|
||||||
import 'package:marco/helpers/services/api_endpoints.dart';
|
|
||||||
import 'package:marco/helpers/utils/mixins/ui_mixin.dart';
|
|
||||||
import 'package:marco/helpers/widgets/my_text.dart';
|
|
||||||
import 'package:marco/images.dart';
|
|
||||||
import 'package:marco/controller/tenant/tenant_selection_controller.dart';
|
|
||||||
|
|
||||||
class TenantSelectionScreen extends StatefulWidget {
|
|
||||||
const TenantSelectionScreen({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<TenantSelectionScreen> createState() => _TenantSelectionScreenState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _TenantSelectionScreenState extends State<TenantSelectionScreen>
|
|
||||||
with UIMixin, SingleTickerProviderStateMixin {
|
|
||||||
late final TenantSelectionController _controller;
|
|
||||||
late final AnimationController _logoAnimController;
|
|
||||||
late final Animation<double> _logoAnimation;
|
|
||||||
final bool _isBetaEnvironment = ApiEndpoints.baseUrl.contains("stage");
|
|
||||||
bool _isLoading = false;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
_controller = Get.put(TenantSelectionController());
|
|
||||||
_logoAnimController = AnimationController(
|
|
||||||
vsync: this,
|
|
||||||
duration: const Duration(milliseconds: 800),
|
|
||||||
);
|
|
||||||
_logoAnimation = CurvedAnimation(
|
|
||||||
parent: _logoAnimController,
|
|
||||||
curve: Curves.easeOutBack,
|
|
||||||
);
|
|
||||||
_logoAnimController.forward();
|
|
||||||
|
|
||||||
// 🔥 Tell controller this is tenant selection screen
|
|
||||||
_controller.loadTenants(fromTenantSelectionScreen: true);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_logoAnimController.dispose();
|
|
||||||
Get.delete<TenantSelectionController>();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _onTenantSelected(String tenantId) async {
|
|
||||||
setState(() => _isLoading = true);
|
|
||||||
await _controller.onTenantSelected(tenantId);
|
|
||||||
setState(() => _isLoading = false);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Scaffold(
|
|
||||||
body: Stack(
|
|
||||||
children: [
|
|
||||||
_RedWaveBackground(brandRed: contentTheme.brandRed),
|
|
||||||
SafeArea(
|
|
||||||
child: Center(
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
const SizedBox(height: 24),
|
|
||||||
_AnimatedLogo(animation: _logoAnimation),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Expanded(
|
|
||||||
child: SingleChildScrollView(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 24),
|
|
||||||
child: Center(
|
|
||||||
child: ConstrainedBox(
|
|
||||||
constraints: const BoxConstraints(maxWidth: 420),
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
const _WelcomeTexts(),
|
|
||||||
if (_isBetaEnvironment) ...[
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
const _BetaBadge(),
|
|
||||||
],
|
|
||||||
const SizedBox(height: 36),
|
|
||||||
// Tenant list directly reacts to controller
|
|
||||||
TenantCardList(
|
|
||||||
controller: _controller,
|
|
||||||
isLoading: _isLoading,
|
|
||||||
onTenantSelected: _onTenantSelected,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _AnimatedLogo extends StatelessWidget {
|
|
||||||
final Animation<double> animation;
|
|
||||||
const _AnimatedLogo({required this.animation});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return ScaleTransition(
|
|
||||||
scale: animation,
|
|
||||||
child: Container(
|
|
||||||
width: 100,
|
|
||||||
height: 100,
|
|
||||||
padding: const EdgeInsets.all(20),
|
|
||||||
decoration: const BoxDecoration(
|
|
||||||
color: Colors.white,
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
boxShadow: [
|
|
||||||
BoxShadow(
|
|
||||||
color: Colors.black12,
|
|
||||||
blurRadius: 10,
|
|
||||||
offset: Offset(0, 4),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
child: Image.asset(Images.logoDark),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _WelcomeTexts extends StatelessWidget {
|
|
||||||
const _WelcomeTexts();
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Column(
|
|
||||||
children: [
|
|
||||||
MyText(
|
|
||||||
"Welcome",
|
|
||||||
fontSize: 24,
|
|
||||||
fontWeight: 600,
|
|
||||||
color: Colors.black87,
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 10),
|
|
||||||
MyText(
|
|
||||||
"Please select which dashboard you want to explore!.",
|
|
||||||
fontSize: 14,
|
|
||||||
color: Colors.black54,
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _BetaBadge extends StatelessWidget {
|
|
||||||
const _BetaBadge();
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Container(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.orangeAccent,
|
|
||||||
borderRadius: BorderRadius.circular(5),
|
|
||||||
),
|
|
||||||
child: MyText(
|
|
||||||
'BETA',
|
|
||||||
color: Colors.white,
|
|
||||||
fontWeight: 600,
|
|
||||||
fontSize: 12,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class TenantCardList extends StatelessWidget {
|
|
||||||
final TenantSelectionController controller;
|
|
||||||
final bool isLoading;
|
|
||||||
final Function(String tenantId) onTenantSelected;
|
|
||||||
|
|
||||||
const TenantCardList({
|
|
||||||
required this.controller,
|
|
||||||
required this.isLoading,
|
|
||||||
required this.onTenantSelected,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Obx(() {
|
|
||||||
if (controller.isLoading.value || isLoading) {
|
|
||||||
return const Center(
|
|
||||||
child: CircularProgressIndicator(strokeWidth: 2),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (controller.tenants.isEmpty) {
|
|
||||||
return Center(
|
|
||||||
child: MyText(
|
|
||||||
"No dashboards available for your account.",
|
|
||||||
fontSize: 14,
|
|
||||||
color: Colors.black54,
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (controller.tenants.length == 1) {
|
|
||||||
return const SizedBox.shrink();
|
|
||||||
}
|
|
||||||
return SingleChildScrollView(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 0, vertical: 24),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
...controller.tenants.map(
|
|
||||||
(tenant) => _TenantCard(
|
|
||||||
tenant: tenant,
|
|
||||||
onTap: () => onTenantSelected(tenant.id),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
TextButton.icon(
|
|
||||||
onPressed: () => Get.back(),
|
|
||||||
icon: const Icon(Icons.arrow_back,
|
|
||||||
size: 20, color: Colors.redAccent),
|
|
||||||
label: MyText(
|
|
||||||
'Back to Login',
|
|
||||||
color: Colors.red,
|
|
||||||
fontWeight: 600,
|
|
||||||
fontSize: 14,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _TenantCard extends StatelessWidget {
|
|
||||||
final dynamic tenant;
|
|
||||||
final VoidCallback onTap;
|
|
||||||
const _TenantCard({required this.tenant, required this.onTap});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return InkWell(
|
|
||||||
onTap: onTap,
|
|
||||||
borderRadius: BorderRadius.circular(5),
|
|
||||||
child: Card(
|
|
||||||
elevation: 3,
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(5),
|
|
||||||
),
|
|
||||||
margin: const EdgeInsets.only(bottom: 20),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
child: Row(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
ClipRRect(
|
|
||||||
borderRadius: BorderRadius.circular(5),
|
|
||||||
child: Container(
|
|
||||||
width: 60,
|
|
||||||
height: 60,
|
|
||||||
color: Colors.grey.shade200,
|
|
||||||
child: TenantLogo(logoImage: tenant.logoImage),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 16),
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
MyText(
|
|
||||||
tenant.name,
|
|
||||||
fontSize: 18,
|
|
||||||
fontWeight: 700,
|
|
||||||
color: Colors.black87,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 6),
|
|
||||||
MyText(
|
|
||||||
"Industry: ${tenant.industry?.name ?? "-"}",
|
|
||||||
fontSize: 13,
|
|
||||||
color: Colors.black54,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Icon(
|
|
||||||
Icons.arrow_forward_ios,
|
|
||||||
size: 24,
|
|
||||||
color: Colors.red,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class TenantLogo extends StatelessWidget {
|
|
||||||
final String? logoImage;
|
|
||||||
const TenantLogo({required this.logoImage});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
if (logoImage == null || logoImage!.isEmpty) {
|
|
||||||
return Center(
|
|
||||||
child: Icon(Icons.business, color: Colors.grey.shade600),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (logoImage!.startsWith("data:image")) {
|
|
||||||
try {
|
|
||||||
final base64Str = logoImage!.split(',').last;
|
|
||||||
final bytes = base64Decode(base64Str);
|
|
||||||
return Image.memory(bytes, fit: BoxFit.cover);
|
|
||||||
} catch (_) {
|
|
||||||
return Center(
|
|
||||||
child: Icon(Icons.business, color: Colors.grey.shade600),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return Image.network(
|
|
||||||
logoImage!,
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
errorBuilder: (_, __, ___) => Center(
|
|
||||||
child: Icon(Icons.business, color: Colors.grey.shade600),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _RedWaveBackground extends StatelessWidget {
|
|
||||||
final Color brandRed;
|
|
||||||
const _RedWaveBackground({required this.brandRed});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return CustomPaint(
|
|
||||||
painter: _WavePainter(brandRed),
|
|
||||||
size: Size.infinite,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _WavePainter extends CustomPainter {
|
|
||||||
final Color brandRed;
|
|
||||||
|
|
||||||
_WavePainter(this.brandRed);
|
|
||||||
|
|
||||||
@override
|
|
||||||
void paint(Canvas canvas, Size size) {
|
|
||||||
final paint1 = Paint()
|
|
||||||
..shader = LinearGradient(
|
|
||||||
colors: [brandRed, const Color.fromARGB(255, 97, 22, 22)],
|
|
||||||
begin: Alignment.topLeft,
|
|
||||||
end: Alignment.bottomRight,
|
|
||||||
).createShader(Rect.fromLTWH(0, 0, size.width, size.height));
|
|
||||||
|
|
||||||
final path1 = Path()
|
|
||||||
..moveTo(0, size.height * 0.2)
|
|
||||||
..quadraticBezierTo(size.width * 0.25, size.height * 0.05,
|
|
||||||
size.width * 0.5, size.height * 0.15)
|
|
||||||
..quadraticBezierTo(
|
|
||||||
size.width * 0.75, size.height * 0.25, size.width, size.height * 0.1)
|
|
||||||
..lineTo(size.width, 0)
|
|
||||||
..lineTo(0, 0)
|
|
||||||
..close();
|
|
||||||
canvas.drawPath(path1, paint1);
|
|
||||||
|
|
||||||
final paint2 = Paint()..color = Colors.redAccent.withOpacity(0.15);
|
|
||||||
final path2 = Path()
|
|
||||||
..moveTo(0, size.height * 0.25)
|
|
||||||
..quadraticBezierTo(
|
|
||||||
size.width * 0.4, size.height * 0.1, size.width, size.height * 0.2)
|
|
||||||
..lineTo(size.width, 0)
|
|
||||||
..lineTo(0, 0)
|
|
||||||
..close();
|
|
||||||
canvas.drawPath(path2, paint2);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool shouldRepaint(CustomPainter oldDelegate) => false;
|
|
||||||
}
|
|
Loading…
x
Reference in New Issue
Block a user