Feature_Report_Action #48

Merged
vaibhav.surve merged 19 commits from Feature_Report_Action into main 2025-06-23 07:32:30 +00:00
8 changed files with 162 additions and 219 deletions
Showing only changes of commit 3ede53713d - Show all commits

View File

@ -1,42 +1,29 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:logger/logger.dart'; import 'package:logger/logger.dart';
import 'package:marco/helpers/services/api_service.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_form_validator.dart';
import 'package:marco/helpers/widgets/my_snackbar.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'; import 'package:marco/model/dailyTaskPlaning/master_work_category_model.dart';
final Logger log = Logger(); final Logger log = Logger();
class AddTaskController extends GetxController { class AddTaskController extends GetxController {
List<ProjectModel> projects = [];
List<EmployeeModel> employees = [];
RxMap<String, RxBool> uploadingStates = <String, RxBool>{}.obs; RxMap<String, RxBool> uploadingStates = <String, RxBool>{}.obs;
MyFormValidator basicValidator = MyFormValidator(); MyFormValidator basicValidator = MyFormValidator();
RxnInt selectedCategoryId = RxnInt(); RxnString selectedCategoryId = RxnString();
RxnString selectedCategoryName = RxnString();
var categoryIdNameMap = <String, String>{}.obs;
List<Map<String, dynamic>> roles = []; List<Map<String, dynamic>> roles = [];
RxnString selectedRoleId = RxnString(); RxnString selectedRoleId = RxnString();
RxList<EmployeeModel> selectedEmployees = <EmployeeModel>[].obs;
RxBool isLoadingWorkMasterCategories = false.obs; RxBool isLoadingWorkMasterCategories = false.obs;
RxList<WorkCategoryModel> workMasterCategories = <WorkCategoryModel>[].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; RxBool isLoading = false.obs;
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
fetchRoles();
fetchWorkMasterCategories(); fetchWorkMasterCategories();
final projectId = Get.find<ProjectController>().selectedProject?.id;
fetchEmployeesByProject(projectId);
} }
String? formFieldValidator(String? value, {required String fieldType}) { String? formFieldValidator(String? value, {required String fieldType}) {
@ -56,23 +43,6 @@ class AddTaskController extends GetxController {
return null; 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({ Future<bool> assignDailyTask({
required String workItemId, required String workItemId,
required int plannedTask, required int plannedTask,
@ -111,10 +81,11 @@ class AddTaskController extends GetxController {
Future<bool> createTask({ Future<bool> createTask({
required String parentTaskId, required String parentTaskId,
required String workAreaId,
required String activityId,
required int plannedTask, required int plannedTask,
required String description, required String comment,
required List<String> taskTeam, required String categoryId,
required String workItemId,
DateTime? assignmentDate, DateTime? assignmentDate,
}) async { }) async {
logger.i("Creating new task..."); logger.i("Creating new task...");
@ -122,10 +93,12 @@ class AddTaskController extends GetxController {
final response = await ApiService.createTask( final response = await ApiService.createTask(
parentTaskId: parentTaskId, parentTaskId: parentTaskId,
plannedTask: plannedTask, plannedTask: plannedTask,
description: description, comment: comment,
taskTeam: taskTeam, workAreaId: workAreaId,
workItemId: workItemId, activityId: activityId,
assignmentDate: assignmentDate, assignmentDate: assignmentDate,
categoryId: categoryId,
); );
if (response == true) { if (response == true) {
@ -147,34 +120,6 @@ class AddTaskController extends GetxController {
} }
} }
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 { Future<void> fetchWorkMasterCategories() async {
isLoadingWorkMasterCategories.value = true; isLoadingWorkMasterCategories.value = true;
@ -182,15 +127,22 @@ class AddTaskController extends GetxController {
if (response != null) { if (response != null) {
try { try {
final dataList = response['data'] ?? []; final dataList = response['data'] ?? [];
workMasterCategories.assignAll(
List<WorkCategoryModel>.from( final parsedList = List<WorkCategoryModel>.from(
dataList.map((e) => WorkCategoryModel.fromJson(e)), dataList.map((e) => WorkCategoryModel.fromJson(e)),
),
); );
workMasterCategories.assignAll(parsedList);
final Map<String, String> mapped = {
for (var item in parsedList) item.id: item.name,
};
categoryIdNameMap.assignAll(mapped);
logger.i("Work categories fetched: ${dataList.length}"); logger.i("Work categories fetched: ${dataList.length}");
} catch (e) { } catch (e) {
logger.e("Error parsing work categories: $e"); logger.e("Error parsing work categories: $e");
workMasterCategories.clear(); workMasterCategories.clear();
categoryIdNameMap.clear();
} }
} else { } else {
logger.w("No work categories found or API call failed."); logger.w("No work categories found or API call failed.");
@ -199,4 +151,9 @@ class AddTaskController extends GetxController {
isLoadingWorkMasterCategories.value = false; isLoadingWorkMasterCategories.value = false;
update(); update();
} }
void selectCategory(String id) {
selectedCategoryId.value = id;
selectedCategoryName.value = categoryIdNameMap[id];
}
} }

View File

@ -26,6 +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 assignTask = "/project/task";
static const String getmasterWorkCategories = "/Master/work-categories"; static const String getmasterWorkCategories = "/Master/work-categories";
} }

View File

@ -408,20 +408,21 @@ class ApiService {
static Future<bool> createTask({ static Future<bool> createTask({
required String parentTaskId, required String parentTaskId,
required int plannedTask, required int plannedTask,
required String description, required String comment,
required List<String> taskTeam, required String workAreaId,
required String workItemId, required String activityId,
DateTime? assignmentDate, DateTime? assignmentDate,
required String categoryId,
}) async { }) async {
final body = { final body = [{
"assignmentDate":
(assignmentDate ?? DateTime.now()).toUtc().toIso8601String(),
"parentTaskId": parentTaskId, "parentTaskId": parentTaskId,
"plannedTask": plannedTask, "plannedWork": plannedTask,
"description": description, "comment": comment,
"taskTeam": taskTeam, "workAreaID": workAreaId,
"workItemId": workItemId, "activityID": activityId,
}; "workCategoryId": categoryId,
'completedWork': 0,
}];
final response = await _postRequest(ApiEndpoints.assignTask, body); final response = await _postRequest(ApiEndpoints.assignTask, body);
if (response == null) return false; if (response == null) return false;

View File

@ -2,8 +2,11 @@ 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/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:logger/logger.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart'; import 'package:marco/helpers/widgets/my_snackbar.dart';
final Logger log = Logger();
void showManageTaskBottomSheet({ void showManageTaskBottomSheet({
required String workArea, required String workArea,
required String activity, required String activity,
@ -12,82 +15,15 @@ void showManageTaskBottomSheet({
required Function(String) onCategoryChanged, required Function(String) onCategoryChanged,
required String parentTaskId, required String parentTaskId,
required int plannedTask, required int plannedTask,
required String workItemId, required String activityId,
required String workAreaId,
required VoidCallback onSubmit, required VoidCallback onSubmit,
}) { }) {
final controller = Get.put(AddTaskController()); final controller = Get.put(AddTaskController());
final ScrollController employeeListScrollController = ScrollController();
final TextEditingController plannedTaskController = final TextEditingController plannedTaskController =
TextEditingController(text: plannedTask.toString()); TextEditingController(text: plannedTask.toString());
final TextEditingController descriptionController = TextEditingController(); 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(
builder: (context, setState) { builder: (context, setState) {
@ -166,7 +102,7 @@ void showManageTaskBottomSheet({
Icon(Icons.description_outlined, Icon(Icons.description_outlined,
color: Colors.grey[700], size: 18), color: Colors.grey[700], size: 18),
const SizedBox(width: 8), const SizedBox(width: 8),
MyText.bodyMedium("Description", fontWeight: 600), MyText.bodyMedium("Comment", fontWeight: 600),
], ],
), ),
const SizedBox(height: 6), const SizedBox(height: 6),
@ -182,48 +118,59 @@ void showManageTaskBottomSheet({
), ),
), ),
const SizedBox(height: 24), const SizedBox(height: 24),
Row( Row(
children: [ children: [
Icon(Icons.group_add_outlined, Icon(Icons.category_outlined,
color: Colors.grey[700], size: 18), color: Colors.grey[700], size: 18),
const SizedBox(width: 8), const SizedBox(width: 8),
MyText.bodyMedium("Select Team Members", fontWeight: 600), MyText.bodyMedium("Selected Work Category",
fontWeight: 600),
], ],
), ),
const SizedBox(height: 8), const SizedBox(height: 6),
Obx(() { Obx(() {
if (controller.isLoading.value) { final categoryMap = controller.categoryIdNameMap;
return const Center( final String selectedName =
child: CircularProgressIndicator()); controller.selectedCategoryId.value != null
} ? (categoryMap[
controller.selectedCategoryId.value!] ??
'Select Category')
: 'Select Category';
return Container( return Container(
constraints: const BoxConstraints(maxHeight: 150), width: double.infinity,
child: buildEmployeeList(), padding: const EdgeInsets.symmetric(
); horizontal: 12, vertical: 14),
}), decoration: BoxDecoration(
const SizedBox(height: 12), border: Border.all(color: Colors.grey.shade300),
Obx(() { borderRadius: BorderRadius.circular(8),
if (controller.selectedEmployees.isEmpty) { ),
return const SizedBox.shrink(); child: PopupMenuButton<String>(
} padding: EdgeInsets.zero,
return Wrap( onSelected: (val) {
spacing: 8, controller.selectCategory(val);
runSpacing: 8, onCategoryChanged(val);
children: },
controller.selectedEmployees.map((employee) { itemBuilder: (context) => categoryMap.entries
return Chip( .map(
label: Text(employee.name), (entry) => PopupMenuItem<String>(
deleteIcon: const Icon(Icons.close), value: entry.key,
onDeleted: () { child: Text(entry.value),
controller.uploadingStates[employee.id]?.value = ),
false; )
controller.updateSelectedEmployees(); .toList(),
}, child: Row(
backgroundColor: Colors.blue.shade100, mainAxisAlignment: MainAxisAlignment.spaceBetween,
labelStyle: const TextStyle(color: Colors.black), children: [
); Text(
}).toList(), selectedName,
style: const TextStyle(
fontSize: 14, color: Colors.black87),
),
const Icon(Icons.arrow_drop_down),
],
),
),
); );
}), }),
const SizedBox(height: 24), const SizedBox(height: 24),
@ -232,7 +179,7 @@ void showManageTaskBottomSheet({
Expanded( Expanded(
child: OutlinedButton( child: OutlinedButton(
onPressed: () { onPressed: () {
Get.back(); // Close bottom sheet Get.back();
}, },
style: OutlinedButton.styleFrom( style: OutlinedButton.styleFrom(
side: const BorderSide(color: Colors.grey), side: const BorderSide(color: Colors.grey),
@ -250,30 +197,42 @@ void showManageTaskBottomSheet({
Expanded( Expanded(
child: ElevatedButton( child: ElevatedButton(
onPressed: () async { onPressed: () async {
final taskTeam = controller.selectedEmployees final plannedValue = int.tryParse(
.map((e) => e.id) plannedTaskController.text.trim()) ??
.toList(); 0;
final comment =
descriptionController.text.trim();
final assignmentDate = DateTime.now();
if (taskTeam.isEmpty) { // 🪵 Log the task creation input values
log.i({
"message": "Creating task with data",
"parentTaskId": parentTaskId,
"plannedTask": plannedValue,
"comment": comment,
"workAreaId": workAreaId,
"activityId": activityId,
});
final selectedCategoryId =
controller.selectedCategoryId.value;
if (selectedCategoryId == null) {
showAppSnackbar( showAppSnackbar(
title: "Team Required", title: "error",
message: message: "Please select a work category!",
"Please select at least one team member.", type: SnackbarType.error,
type: SnackbarType.warning,
); );
return; return;
} }
final success = await controller.createTask( final success = await controller.createTask(
parentTaskId: parentTaskId, parentTaskId: parentTaskId,
plannedTask: int.tryParse( plannedTask: plannedValue,
plannedTaskController.text.trim()) ?? comment: comment,
0, workAreaId: workAreaId,
description: activityId: activityId,
descriptionController.text.trim(), categoryId:
taskTeam: taskTeam, selectedCategoryId,
workItemId: workItemId,
assignmentDate: DateTime.now(),
); );
if (success) { if (success) {

View File

@ -18,14 +18,17 @@ 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 taskDataId;
final String workItemId; final String workAreaId;
final String activityId;
final VoidCallback onReportSuccess; 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.taskDataId,
required this.workItemId, required this.workAreaId,
required this.activityId,
required this.onReportSuccess, required this.onReportSuccess,
}); });
@ -77,8 +80,7 @@ class _ReportActionBottomSheetState extends State<ReportActionBottomSheet>
controller.basicValidator.getController('comment')?.clear(); controller.basicValidator.getController('comment')?.clear();
controller.basicValidator.getController('task_id')?.text = controller.basicValidator.getController('task_id')?.text =
widget.taskDataId; widget.taskDataId;
controller.basicValidator.getController('work_item_id')?.text =
widget.workItemId;
controller.selectedImages.clear(); controller.selectedImages.clear();
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
@ -486,7 +488,10 @@ class _ReportActionBottomSheetState extends State<ReportActionBottomSheet>
widget.taskData['plannedWork'] ?? widget.taskData['plannedWork'] ??
'0') ?? '0') ??
0, 0,
workItemId: widget.workItemId, activityId:
widget.activityId,
workAreaId:
widget.workAreaId,
onSubmit: () { onSubmit: () {
Navigator.of(context).pop(); Navigator.of(context).pop();
}, },

View File

@ -106,7 +106,8 @@ class TaskActionButtons {
required int completed, required int completed,
required VoidCallback refreshCallback, required VoidCallback refreshCallback,
required String parentTaskID, required String parentTaskID,
required String workItemId, required String activityId,
required String workAreaId,
}) { }) {
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),
@ -131,7 +132,8 @@ class TaskActionButtons {
child: ReportActionBottomSheet( child: ReportActionBottomSheet(
taskData: taskData, taskData: taskData,
taskDataId: parentTaskID, taskDataId: parentTaskID,
workItemId: workItemId, workAreaId: workAreaId,
activityId: activityId,
onReportSuccess: refreshCallback, onReportSuccess: refreshCallback,
), ),
), ),

View File

@ -32,8 +32,9 @@ class TaskModel {
? DateTime.tryParse(json['reportedDate']) ? DateTime.tryParse(json['reportedDate'])
: null, : null,
id: json['id'], id: json['id'],
workItem: workItem: json['workItem'] != null
json['workItem'] != null ? WorkItem.fromJson(json['workItem']) : null, ? WorkItem.fromJson(json['workItem'])
: null,
workItemId: json['workItemId'], workItemId: json['workItemId'],
plannedTask: json['plannedTask'], plannedTask: json['plannedTask'],
completedTask: json['completedTask'], completedTask: json['completedTask'],
@ -87,25 +88,39 @@ class WorkItem {
} }
class ActivityMaster { class ActivityMaster {
final String? id; // Added
final String activityName; final String activityName;
ActivityMaster({required this.activityName}); ActivityMaster({
this.id,
required this.activityName,
});
factory ActivityMaster.fromJson(Map<String, dynamic> json) { factory ActivityMaster.fromJson(Map<String, dynamic> json) {
return ActivityMaster(activityName: json['activityName'] ?? ''); return ActivityMaster(
id: json['id']?.toString(),
activityName: json['activityName'] ?? '',
);
} }
} }
class WorkArea { class WorkArea {
final String? id; // Added
final String areaName; final String areaName;
final Floor? floor; final Floor? floor;
WorkArea({required this.areaName, this.floor}); WorkArea({
this.id,
required this.areaName,
this.floor,
});
factory WorkArea.fromJson(Map<String, dynamic> json) { factory WorkArea.fromJson(Map<String, dynamic> json) {
return WorkArea( return WorkArea(
id: json['id']?.toString(),
areaName: json['areaName'] ?? '', areaName: json['areaName'] ?? '',
floor: json['floor'] != null ? Floor.fromJson(json['floor']) : null, floor:
json['floor'] != null ? Floor.fromJson(json['floor']) : null,
); );
} }
} }

View File

@ -368,6 +368,9 @@ class _DailyProgressReportScreenState extends State<DailyProgressReportScreen>
final activityName = final activityName =
task.workItem?.activityMaster?.activityName ?? 'N/A'; task.workItem?.activityMaster?.activityName ?? 'N/A';
final activityId = task.workItem?.activityMaster?.id ;
final workAreaId =
task.workItem?.workArea?.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,
@ -380,7 +383,6 @@ class _DailyProgressReportScreenState extends State<DailyProgressReportScreen>
? (completed / planned).clamp(0.0, 1.0) ? (completed / planned).clamp(0.0, 1.0)
: 0.0; : 0.0;
final parentTaskID = task.id; final parentTaskID = task.id;
final workItemId = task.workItem?.id;
return Column( return Column(
children: [ children: [
Padding( Padding(
@ -474,10 +476,12 @@ class _DailyProgressReportScreenState extends State<DailyProgressReportScreen>
context: context, context: context,
task: task, task: task,
parentTaskID: parentTaskID, parentTaskID: parentTaskID,
workItemId: workItemId.toString(), workAreaId: workAreaId.toString(),
activityId: activityId.toString(),
completed: completed, completed: completed,
refreshCallback: _refreshData, refreshCallback: _refreshData,
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
], ],
TaskActionButtons.commentButton( TaskActionButtons.commentButton(