Feature_Report_Action #48

Merged
vaibhav.surve merged 19 commits from Feature_Report_Action into main 2025-06-23 07:32:30 +00:00
9 changed files with 572 additions and 134 deletions
Showing only changes of commit 206c84b3a1 - Show all commits

View File

@ -0,0 +1,202 @@
import 'package:get/get.dart';
import 'package:logger/logger.dart';
import 'package:marco/helpers/services/api_service.dart';
import 'package:marco/model/project_model.dart';
import 'package:marco/model/employee_model.dart';
import 'package:marco/helpers/widgets/my_form_validator.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart';
import 'package:marco/controller/project_controller.dart';
import 'package:marco/model/dailyTaskPlaning/master_work_category_model.dart';
final Logger log = Logger();
class AddTaskController extends GetxController {
List<ProjectModel> projects = [];
List<EmployeeModel> employees = [];
RxMap<String, RxBool> uploadingStates = <String, RxBool>{}.obs;
MyFormValidator basicValidator = MyFormValidator();
RxnInt selectedCategoryId = RxnInt();
List<Map<String, dynamic>> roles = [];
RxnString selectedRoleId = RxnString();
RxList<EmployeeModel> selectedEmployees = <EmployeeModel>[].obs;
RxBool isLoadingWorkMasterCategories = false.obs;
RxList<WorkCategoryModel> workMasterCategories = <WorkCategoryModel>[].obs;
void updateSelectedEmployees() {
final selected =
employees.where((e) => uploadingStates[e.id]?.value == true).toList();
selectedEmployees.value = selected;
}
RxBool isLoading = false.obs;
@override
void onInit() {
super.onInit();
fetchRoles();
fetchWorkMasterCategories();
final projectId = Get.find<ProjectController>().selectedProject?.id;
fetchEmployeesByProject(projectId);
}
String? formFieldValidator(String? value, {required String fieldType}) {
if (value == null || value.trim().isEmpty) {
return 'This field is required';
}
if (fieldType == "target") {
if (int.tryParse(value.trim()) == null) {
return 'Please enter a valid number';
}
}
if (fieldType == "description") {
if (value.trim().length < 5) {
return 'Description must be at least 5 characters';
}
}
return null;
}
Future<void> fetchRoles() async {
logger.i("Fetching roles...");
final result = await ApiService.getRoles();
if (result != null) {
roles = List<Map<String, dynamic>>.from(result);
logger.i("Roles fetched successfully.");
update();
} else {
logger.e("Failed to fetch roles.");
}
}
void onRoleSelected(String? roleId) {
selectedRoleId.value = roleId;
logger.i("Role selected: $roleId");
}
Future<bool> assignDailyTask({
required String workItemId,
required int plannedTask,
required String description,
required List<String> taskTeam,
DateTime? assignmentDate,
}) async {
logger.i("Starting assign task...");
final response = await ApiService.assignDailyTask(
workItemId: workItemId,
plannedTask: plannedTask,
description: description,
taskTeam: taskTeam,
assignmentDate: assignmentDate,
);
if (response == true) {
logger.i("Task assigned successfully.");
showAppSnackbar(
title: "Success",
message: "Task assigned successfully!",
type: SnackbarType.success,
);
return true;
} else {
logger.e("Failed to assign task.");
showAppSnackbar(
title: "Error",
message: "Failed to assign task.",
type: SnackbarType.error,
);
return false;
}
}
Future<bool> createTask({
required String parentTaskId,
required int plannedTask,
required String description,
required List<String> taskTeam,
required String workItemId,
DateTime? assignmentDate,
}) async {
logger.i("Creating new task...");
final response = await ApiService.createTask(
parentTaskId: parentTaskId,
plannedTask: plannedTask,
description: description,
taskTeam: taskTeam,
workItemId: workItemId,
assignmentDate: assignmentDate,
);
if (response == true) {
logger.i("Task created successfully.");
showAppSnackbar(
title: "Success",
message: "Task created successfully!",
type: SnackbarType.success,
);
return true;
} else {
logger.e("Failed to create task.");
showAppSnackbar(
title: "Error",
message: "Failed to create task.",
type: SnackbarType.error,
);
return false;
}
}
Future<void> fetchEmployeesByProject(String? projectId) async {
if (projectId == null || projectId.isEmpty) {
log.e("Project ID is required but was null or empty.");
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;
}
log.i("Employees fetched: ${employees.length} for project $projectId");
} else {
log.w("No employees found for project $projectId.");
employees = [];
}
} catch (e) {
log.e("Error fetching employees for project $projectId: $e");
}
update();
isLoading.value = false;
}
Future<void> fetchWorkMasterCategories() async {
isLoadingWorkMasterCategories.value = true;
final response = await ApiService.getMasterWorkCategories();
if (response != null) {
try {
final dataList = response['data'] ?? [];
workMasterCategories.assignAll(
List<WorkCategoryModel>.from(
dataList.map((e) => WorkCategoryModel.fromJson(e)),
),
);
logger.i("Work categories fetched: ${dataList.length}");
} catch (e) {
logger.e("Error parsing work categories: $e");
workMasterCategories.clear();
}
} else {
logger.w("No work categories found or API call failed.");
}
isLoadingWorkMasterCategories.value = false;
update();
}
}

View File

@ -26,4 +26,6 @@ class ApiEndpoints {
static const String assignDailyTask = "/task/assign"; static const String assignDailyTask = "/task/assign";
static const String getWorkStatus = "/master/work-status"; static const String getWorkStatus = "/master/work-status";
static const String approveReportAction = "/task/approve"; static const String approveReportAction = "/task/approve";
static const String assignTask = "/task/assign";
static const String getmasterWorkCategories = "/Master/work-categories";
} }

View File

@ -378,6 +378,11 @@ class ApiService {
_getRequest(ApiEndpoints.getWorkStatus).then((res) => res != null _getRequest(ApiEndpoints.getWorkStatus).then((res) => res != null
? _parseResponseForAllData(res, label: 'Work Status') ? _parseResponseForAllData(res, label: 'Work Status')
: null); : null);
static Future<Map<String, dynamic>?> getMasterWorkCategories() async =>
_getRequest(ApiEndpoints.getmasterWorkCategories).then((res) =>
res != null
? _parseResponseForAllData(res, label: 'Master Work Categories')
: null);
static Future<bool> approveTask({ static Future<bool> approveTask({
required String id, required String id,
required String comment, required String comment,
@ -399,4 +404,35 @@ class ApiService {
final json = jsonDecode(response.body); final json = jsonDecode(response.body);
return response.statusCode == 200 && json['success'] == true; return response.statusCode == 200 && json['success'] == true;
} }
static Future<bool> createTask({
required String parentTaskId,
required int plannedTask,
required String description,
required List<String> taskTeam,
required String workItemId,
DateTime? assignmentDate,
}) async {
final body = {
"assignmentDate":
(assignmentDate ?? DateTime.now()).toUtc().toIso8601String(),
"parentTaskId": parentTaskId,
"plannedTask": plannedTask,
"description": description,
"taskTeam": taskTeam,
"workItemId": workItemId,
};
final response = await _postRequest(ApiEndpoints.assignTask, body);
if (response == null) return false;
final json = jsonDecode(response.body);
if (response.statusCode == 200 && json['success'] == true) {
Get.back();
return true;
}
_log("Failed to create task: ${json['message'] ?? 'Unknown error'}");
return false;
}
} }

View File

@ -1,20 +1,92 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:marco/controller/task_planing/add_task_controller.dart';
import 'package:marco/helpers/widgets/my_text.dart'; import 'package:marco/helpers/widgets/my_text.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart';
void showManageTaskBottomSheet({ void showManageTaskBottomSheet({
required String building,
required String floor,
required String workArea, required String workArea,
required String activity, required String activity,
required String plannedWork,
required String completedWork, required String completedWork,
required String unit, required String unit,
required Function(String) onCategoryChanged, required Function(String) onCategoryChanged,
required String parentTaskId,
required int plannedTask,
required String workItemId,
required VoidCallback onSubmit, required VoidCallback onSubmit,
}) { }) {
final List<String> categories = ["Fresh Work", "Repair", "Demo"]; final controller = Get.put(AddTaskController());
String selectedCategory = 'Fresh Work'; final ScrollController employeeListScrollController = ScrollController();
final TextEditingController plannedTaskController =
TextEditingController(text: plannedTask.toString());
final TextEditingController descriptionController = TextEditingController();
Widget buildEmployeeList() {
final selectedRoleId = controller.selectedRoleId.value;
final filteredEmployees = selectedRoleId == null
? controller.employees
: controller.employees
.where((e) => e.jobRoleID.toString() == selectedRoleId)
.toList();
if (filteredEmployees.isEmpty) {
return MyText.bodySmall("No employees found for selected role.");
}
return Scrollbar(
controller: employeeListScrollController,
thumbVisibility: true,
interactive: true,
child: ListView.builder(
controller: employeeListScrollController,
shrinkWrap: true,
physics: const AlwaysScrollableScrollPhysics(),
itemCount: filteredEmployees.length,
itemBuilder: (context, index) {
final employee = filteredEmployees[index];
final rxBool = controller.uploadingStates[employee.id];
return Obx(() => Padding(
padding: const EdgeInsets.symmetric(vertical: 0),
child: Row(
children: [
Theme(
data: Theme.of(context)
.copyWith(unselectedWidgetColor: Colors.black),
child: Checkbox(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(4),
side: const BorderSide(color: Colors.black),
),
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: MyText.bodySmall(employee.name),
),
],
),
));
},
),
);
}
Get.bottomSheet( Get.bottomSheet(
StatefulBuilder( StatefulBuilder(
@ -22,12 +94,14 @@ void showManageTaskBottomSheet({
return LayoutBuilder( return LayoutBuilder(
builder: (context, constraints) { builder: (context, constraints) {
final isLarge = constraints.maxWidth > 600; final isLarge = constraints.maxWidth > 600;
final horizontalPadding = isLarge ? constraints.maxWidth * 0.2 : 16.0; final horizontalPadding =
isLarge ? constraints.maxWidth * 0.2 : 16.0;
return SafeArea( return SafeArea(
child: Container( child: Container(
constraints: const BoxConstraints(maxHeight: 0.95 * 800), constraints: const BoxConstraints(maxHeight: 760),
padding: EdgeInsets.fromLTRB(horizontalPadding, 12, horizontalPadding, 24), padding: EdgeInsets.fromLTRB(
horizontalPadding, 12, horizontalPadding, 24),
decoration: const BoxDecoration( decoration: const BoxDecoration(
color: Colors.white, color: Colors.white,
borderRadius: BorderRadius.vertical(top: Radius.circular(20)), borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
@ -36,7 +110,6 @@ void showManageTaskBottomSheet({
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// Draggable handle
Center( Center(
child: Container( child: Container(
width: 40, width: 40,
@ -48,74 +121,155 @@ void showManageTaskBottomSheet({
), ),
), ),
), ),
_infoRowWithIcon(
_infoRowWithIcon(Icons.location_city, "Selected Building", building), Icons.workspaces, "Selected Work Area", workArea),
_infoRowWithIcon(Icons.apartment, "Selected Floor", floor), _infoRowWithIcon(
_infoRowWithIcon(Icons.workspaces, "Selected Work Area", workArea), Icons.list_alt, "Selected Activity", activity),
_infoRowWithIcon(Icons.list_alt, "Selected Activity", activity),
const SizedBox(height: 12), const SizedBox(height: 12),
Row( Row(
children: [ children: [
Icon(Icons.category_outlined, color: Colors.grey[700], size: 18), Icon(Icons.edit_calendar,
color: Colors.grey[700], size: 18),
const SizedBox(width: 8), const SizedBox(width: 8),
MyText.bodyMedium("Selected Work Category", fontWeight: 600), MyText.bodyMedium("Planned Work", fontWeight: 600),
], ],
), ),
const SizedBox(height: 6), const SizedBox(height: 6),
TextField(
Container( controller: plannedTaskController,
width: double.infinity, keyboardType: TextInputType.number,
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 14), decoration: InputDecoration(
decoration: BoxDecoration( hintText: "Enter planned work",
border: Border.all(color: Colors.grey.shade300), border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8)),
), contentPadding: const EdgeInsets.symmetric(
child: PopupMenuButton<String>( horizontal: 12, vertical: 10),
padding: EdgeInsets.zero,
onSelected: (val) {
setState(() {
selectedCategory = val;
});
onCategoryChanged(val);
},
itemBuilder: (context) => categories
.map((e) => PopupMenuItem(value: e, child: Text(e)))
.toList(),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
selectedCategory,
style: TextStyle(fontSize: 14, color: Colors.black87),
),
const Icon(Icons.arrow_drop_down),
],
),
), ),
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
_infoRowWithIcon(Icons.edit_calendar, "Planned Work", plannedWork), _infoRowWithIcon(Icons.check_circle_outline,
_infoRowWithIcon(Icons.check_circle_outline, "Completed Work", completedWork), "Completed Work", completedWork),
_infoRowWithIcon(Icons.straighten, "Unit", unit), const SizedBox(height: 16),
MyText.bodyMedium("Description", fontWeight: 700),
const SizedBox(height: 24), const SizedBox(height: 6),
SizedBox( TextField(
width: double.infinity, controller: descriptionController,
height: 48, maxLines: 3,
child: ElevatedButton( decoration: InputDecoration(
onPressed: onSubmit, hintText: "Enter task description",
style: ElevatedButton.styleFrom( border: OutlineInputBorder(
backgroundColor: Colors.blueAccent, borderRadius: BorderRadius.circular(8)),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), contentPadding: const EdgeInsets.symmetric(
), horizontal: 12, vertical: 10),
child: const Text(
"Submit",
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
),
), ),
), ),
const SizedBox(height: 24),
MyText.bodyMedium("Select Team Members", fontWeight: 700),
const SizedBox(height: 8),
Obx(() {
if (controller.isLoading.value) {
return const Center(
child: CircularProgressIndicator());
}
return Container(
constraints: const BoxConstraints(maxHeight: 150),
child: buildEmployeeList(),
);
}),
const SizedBox(height: 12),
Obx(() {
if (controller.selectedEmployees.isEmpty) {
return const SizedBox.shrink();
}
return Wrap(
spacing: 8,
runSpacing: 8,
children:
controller.selectedEmployees.map((employee) {
return Chip(
label: Text(employee.name),
deleteIcon: const Icon(Icons.close),
onDeleted: () {
controller.uploadingStates[employee.id]?.value =
false;
controller.updateSelectedEmployees();
},
backgroundColor: Colors.blue.shade100,
labelStyle: const TextStyle(color: Colors.black),
);
}).toList(),
);
}),
const SizedBox(height: 24),
Row(
children: [
Expanded(
child: OutlinedButton(
onPressed: () {
Get.back(); // Close bottom sheet
},
style: OutlinedButton.styleFrom(
side: const BorderSide(color: Colors.grey),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10)),
),
child: MyText.bodyMedium(
"Cancel",
fontWeight: 600,
color: Colors.black,
),
),
),
const SizedBox(width: 12),
Expanded(
child: ElevatedButton(
onPressed: () async {
final taskTeam = controller.selectedEmployees
.map((e) => e.id)
.toList();
if (taskTeam.isEmpty) {
showAppSnackbar(
title: "Team Required",
message:
"Please select at least one team member.",
type: SnackbarType.warning,
);
return;
}
final success = await controller.createTask(
parentTaskId: parentTaskId,
plannedTask: int.tryParse(
plannedTaskController.text.trim()) ??
0,
description:
descriptionController.text.trim(),
taskTeam: taskTeam,
workItemId: workItemId,
assignmentDate: DateTime.now(),
);
if (success) {
Get.back();
onSubmit();
}
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blueAccent,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10)),
),
child: MyText.bodyMedium(
"Submit",
fontWeight: 600,
color: Colors.white,
),
),
),
],
),
], ],
), ),
), ),

View File

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

View File

@ -17,11 +17,16 @@ import 'dart:io';
class ReportActionBottomSheet extends StatefulWidget { class ReportActionBottomSheet extends StatefulWidget {
final Map<String, dynamic> taskData; final Map<String, dynamic> taskData;
final VoidCallback? onCommentSuccess; final VoidCallback? onCommentSuccess;
final String taskDataId;
final String workItemId;
final VoidCallback onReportSuccess;
const ReportActionBottomSheet({ const ReportActionBottomSheet({
super.key, super.key,
required this.taskData, required this.taskData,
this.onCommentSuccess, this.onCommentSuccess,
required this.taskDataId,
required this.workItemId,
required this.onReportSuccess,
}); });
@override @override
@ -70,7 +75,10 @@ class _ReportActionBottomSheetState extends State<ReportActionBottomSheet>
controller.basicValidator.getController('task_id')?.text = controller.basicValidator.getController('task_id')?.text =
data['taskId'] ?? ''; data['taskId'] ?? '';
controller.basicValidator.getController('comment')?.clear(); controller.basicValidator.getController('comment')?.clear();
controller.basicValidator.getController('task_id')?.text =
widget.taskDataId;
controller.basicValidator.getController('work_item_id')?.text =
widget.workItemId;
controller.selectedImages.clear(); controller.selectedImages.clear();
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
@ -461,16 +469,11 @@ class _ReportActionBottomSheetState extends State<ReportActionBottomSheet>
if (success) { if (success) {
Navigator.of(context).pop(); Navigator.of(context).pop();
if (shouldShowAddTaskSheet) { if (shouldShowAddTaskSheet) {
await Future.delayed(Duration( await Future.delayed(
milliseconds: Duration(milliseconds: 100));
100)); // Ensure animation completes
showManageTaskBottomSheet( showManageTaskBottomSheet(
building: widget.taskData['building'] ?? '',
floor: widget.taskData['floor'] ?? '',
workArea: widget.taskData['location'] ?? '', workArea: widget.taskData['location'] ?? '',
activity: widget.taskData['activity'] ?? '', activity: widget.taskData['activity'] ?? '',
plannedWork:
widget.taskData['plannedWork'] ?? '',
completedWork: completedWork:
widget.taskData['completedWork'] ?? '', widget.taskData['completedWork'] ?? '',
unit: widget.taskData['unit'] ?? '', unit: widget.taskData['unit'] ?? '',
@ -478,15 +481,18 @@ class _ReportActionBottomSheetState extends State<ReportActionBottomSheet>
debugPrint( debugPrint(
"Category changed to: $category"); "Category changed to: $category");
}, },
parentTaskId: widget.taskDataId,
plannedTask: int.tryParse(
widget.taskData['plannedWork'] ??
'0') ??
0,
workItemId: widget.workItemId,
onSubmit: () { onSubmit: () {
Navigator.of(context) Navigator.of(context).pop();
.pop(); // Close second sheet
}, },
); );
} }
widget.onReportSuccess.call();
// Optional callback after comment success
widget.onCommentSuccess?.call();
} }
} }
}, },

View File

@ -82,7 +82,8 @@ class TaskActionButtons {
textStyle: const TextStyle(fontSize: 14), textStyle: const TextStyle(fontSize: 14),
), ),
onPressed: () { onPressed: () {
final taskData = _prepareTaskData(task: task, completed: task.completedTask); final taskData =
_prepareTaskData(task: task, completed: task.completedTask);
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
isScrollControlled: true, isScrollControlled: true,
@ -104,6 +105,8 @@ class TaskActionButtons {
required dynamic task, required dynamic task,
required int completed, required int completed,
required VoidCallback refreshCallback, required VoidCallback refreshCallback,
required String parentTaskID,
required String workItemId,
}) { }) {
return OutlinedButton.icon( return OutlinedButton.icon(
icon: const Icon(Icons.report, size: 18, color: Colors.amber), icon: const Icon(Icons.report, size: 18, color: Colors.amber),
@ -115,7 +118,7 @@ class TaskActionButtons {
textStyle: const TextStyle(fontSize: 14), textStyle: const TextStyle(fontSize: 14),
), ),
onPressed: () { onPressed: () {
final taskData = _prepareTaskData(task: task, completed: completed); final taskData = _prepareTaskData(task: task, completed: completed);
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
@ -127,6 +130,9 @@ class TaskActionButtons {
padding: MediaQuery.of(ctx).viewInsets, padding: MediaQuery.of(ctx).viewInsets,
child: ReportActionBottomSheet( child: ReportActionBottomSheet(
taskData: taskData, taskData: taskData,
taskDataId: parentTaskID,
workItemId: workItemId,
onReportSuccess: refreshCallback,
), ),
), ),
); );
@ -135,61 +141,61 @@ class TaskActionButtons {
} }
static Map<String, dynamic> _prepareTaskData({ static Map<String, dynamic> _prepareTaskData({
required dynamic task, required dynamic task,
required int completed, required int completed,
}) { }) {
final activityName = task.workItem?.activityMaster?.activityName ?? 'N/A'; final activityName = task.workItem?.activityMaster?.activityName ?? 'N/A';
final assigned = '${(task.plannedTask - completed)}'; final assigned = '${(task.plannedTask - completed)}';
final assignedBy = final assignedBy =
"${task.assignedBy.firstName} ${task.assignedBy.lastName ?? ''}"; "${task.assignedBy.firstName} ${task.assignedBy.lastName ?? ''}";
final assignedOn = DateFormat('yyyy-MM-dd').format(task.assignmentDate); final assignedOn = DateFormat('yyyy-MM-dd').format(task.assignmentDate);
final taskId = task.id; final taskId = task.id;
final location = [ final location = [
task.workItem?.workArea?.floor?.building?.name, task.workItem?.workArea?.floor?.building?.name,
task.workItem?.workArea?.floor?.floorName, task.workItem?.workArea?.floor?.floorName,
task.workItem?.workArea?.areaName, task.workItem?.workArea?.areaName,
].where((e) => e != null && e.isNotEmpty).join(' > '); ].where((e) => e != null && e.isNotEmpty).join(' > ');
final teamMembers = task.teamMembers final teamMembers = task.teamMembers
.map((e) => '${e.firstName} ${e.lastName ?? ''}') .map((e) => '${e.firstName} ${e.lastName ?? ''}')
.toList(); .toList();
final pendingWork = final pendingWork =
(task.workItem?.plannedWork ?? 0) - (task.workItem?.completedWork ?? 0); (task.workItem?.plannedWork ?? 0) - (task.workItem?.completedWork ?? 0);
final taskComments = task.comments.map((comment) { final taskComments = task.comments.map((comment) {
final isoDate = comment.timestamp.toIso8601String(); final isoDate = comment.timestamp.toIso8601String();
final commenterName = comment.commentedBy.firstName.isNotEmpty final commenterName = comment.commentedBy.firstName.isNotEmpty
? "${comment.commentedBy.firstName} ${comment.commentedBy.lastName ?? ''}".trim() ? "${comment.commentedBy.firstName} ${comment.commentedBy.lastName ?? ''}"
: "Unknown"; .trim()
: "Unknown";
return {
'text': comment.comment,
'date': isoDate,
'commentedBy': commenterName,
'preSignedUrls': comment.preSignedUrls,
};
}).toList();
final taskLevelPreSignedUrls = task.reportedPreSignedUrls;
return { return {
'text': comment.comment, 'activity': activityName,
'date': isoDate, 'assigned': assigned,
'commentedBy': commenterName, 'taskId': taskId,
'preSignedUrls': comment.preSignedUrls, '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,
}; };
}).toList(); }
final taskLevelPreSignedUrls = task.reportedPreSignedUrls;
return {
'activity': activityName,
'assigned': assigned,
'taskId': taskId,
'assignedBy': assignedBy,
'completed': completed,
'plannedWork': task.plannedTask.toString(),
'completedWork': completed.toString(),
'assignedOn': assignedOn,
'location': location,
'teamSize': task.teamMembers.length,
'teamMembers': teamMembers,
'pendingWork': pendingWork,
'taskComments': taskComments,
'reportedPreSignedUrls': taskLevelPreSignedUrls,
};
}
} }

View File

@ -379,7 +379,8 @@ class _DailyProgressReportScreenState extends State<DailyProgressReportScreen>
final progress = (planned != 0) final progress = (planned != 0)
? (completed / planned).clamp(0.0, 1.0) ? (completed / planned).clamp(0.0, 1.0)
: 0.0; : 0.0;
final parentTaskID = task.id;
final workItemId = task.workItem?.id;
return Column( return Column(
children: [ children: [
Padding( Padding(
@ -472,6 +473,8 @@ class _DailyProgressReportScreenState extends State<DailyProgressReportScreen>
TaskActionButtons.reportActionButton( TaskActionButtons.reportActionButton(
context: context, context: context,
task: task, task: task,
parentTaskID: parentTaskID,
workItemId: workItemId.toString(),
completed: completed, completed: completed,
refreshCallback: _refreshData, refreshCallback: _refreshData,
), ),

View File

@ -98,8 +98,6 @@ flutter:
- assets/avatar/ - assets/avatar/
- assets/coin/ - assets/coin/
- assets/country/ - assets/country/
- assets/data/
- assets/dummy/
- assets/lang/ - assets/lang/
- assets/logo/ - assets/logo/
- assets/logo/loading_logo.png - assets/logo/loading_logo.png