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: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();
RxnString selectedCategoryId = RxnString();
RxnString selectedCategoryName = RxnString();
var categoryIdNameMap = <String, String>{}.obs;
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}) {
@ -56,23 +43,6 @@ class AddTaskController extends GetxController {
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,
@ -111,10 +81,11 @@ class AddTaskController extends GetxController {
Future<bool> createTask({
required String parentTaskId,
required String workAreaId,
required String activityId,
required int plannedTask,
required String description,
required List<String> taskTeam,
required String workItemId,
required String comment,
required String categoryId,
DateTime? assignmentDate,
}) async {
logger.i("Creating new task...");
@ -122,10 +93,12 @@ class AddTaskController extends GetxController {
final response = await ApiService.createTask(
parentTaskId: parentTaskId,
plannedTask: plannedTask,
description: description,
taskTeam: taskTeam,
workItemId: workItemId,
comment: comment,
workAreaId: workAreaId,
activityId: activityId,
assignmentDate: assignmentDate,
categoryId: categoryId,
);
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 {
isLoadingWorkMasterCategories.value = true;
@ -182,15 +127,22 @@ class AddTaskController extends GetxController {
if (response != null) {
try {
final dataList = response['data'] ?? [];
workMasterCategories.assignAll(
List<WorkCategoryModel>.from(
dataList.map((e) => WorkCategoryModel.fromJson(e)),
),
final parsedList = List<WorkCategoryModel>.from(
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}");
} catch (e) {
logger.e("Error parsing work categories: $e");
workMasterCategories.clear();
categoryIdNameMap.clear();
}
} else {
logger.w("No work categories found or API call failed.");
@ -199,4 +151,9 @@ class AddTaskController extends GetxController {
isLoadingWorkMasterCategories.value = false;
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 getWorkStatus = "/master/work-status";
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";
}

View File

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

View File

@ -2,8 +2,11 @@ import 'package:flutter/material.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:logger/logger.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart';
final Logger log = Logger();
void showManageTaskBottomSheet({
required String workArea,
required String activity,
@ -12,82 +15,15 @@ void showManageTaskBottomSheet({
required Function(String) onCategoryChanged,
required String parentTaskId,
required int plannedTask,
required String workItemId,
required String activityId,
required String workAreaId,
required VoidCallback onSubmit,
}) {
final controller = Get.put(AddTaskController());
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(
StatefulBuilder(
builder: (context, setState) {
@ -166,7 +102,7 @@ void showManageTaskBottomSheet({
Icon(Icons.description_outlined,
color: Colors.grey[700], size: 18),
const SizedBox(width: 8),
MyText.bodyMedium("Description", fontWeight: 600),
MyText.bodyMedium("Comment", fontWeight: 600),
],
),
const SizedBox(height: 6),
@ -182,48 +118,59 @@ void showManageTaskBottomSheet({
),
),
const SizedBox(height: 24),
Row(
Row(
children: [
Icon(Icons.group_add_outlined,
Icon(Icons.category_outlined,
color: Colors.grey[700], size: 18),
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(() {
if (controller.isLoading.value) {
return const Center(
child: CircularProgressIndicator());
}
final categoryMap = controller.categoryIdNameMap;
final String selectedName =
controller.selectedCategoryId.value != null
? (categoryMap[
controller.selectedCategoryId.value!] ??
'Select Category')
: 'Select Category';
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(),
width: double.infinity,
padding: const EdgeInsets.symmetric(
horizontal: 12, vertical: 14),
decoration: BoxDecoration(
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),
],
),
),
);
}),
const SizedBox(height: 24),
@ -232,7 +179,7 @@ void showManageTaskBottomSheet({
Expanded(
child: OutlinedButton(
onPressed: () {
Get.back(); // Close bottom sheet
Get.back();
},
style: OutlinedButton.styleFrom(
side: const BorderSide(color: Colors.grey),
@ -250,30 +197,42 @@ void showManageTaskBottomSheet({
Expanded(
child: ElevatedButton(
onPressed: () async {
final taskTeam = controller.selectedEmployees
.map((e) => e.id)
.toList();
final plannedValue = int.tryParse(
plannedTaskController.text.trim()) ??
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(
title: "Team Required",
message:
"Please select at least one team member.",
type: SnackbarType.warning,
title: "error",
message: "Please select a work category!",
type: SnackbarType.error,
);
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(),
plannedTask: plannedValue,
comment: comment,
workAreaId: workAreaId,
activityId: activityId,
categoryId:
selectedCategoryId,
);
if (success) {

View File

@ -18,14 +18,17 @@ class ReportActionBottomSheet extends StatefulWidget {
final Map<String, dynamic> taskData;
final VoidCallback? onCommentSuccess;
final String taskDataId;
final String workItemId;
final String workAreaId;
final String activityId;
final VoidCallback onReportSuccess;
const ReportActionBottomSheet({
super.key,
required this.taskData,
this.onCommentSuccess,
required this.taskDataId,
required this.workItemId,
required this.workAreaId,
required this.activityId,
required this.onReportSuccess,
});
@ -77,8 +80,7 @@ class _ReportActionBottomSheetState extends State<ReportActionBottomSheet>
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();
WidgetsBinding.instance.addPostFrameCallback((_) {
@ -486,7 +488,10 @@ class _ReportActionBottomSheetState extends State<ReportActionBottomSheet>
widget.taskData['plannedWork'] ??
'0') ??
0,
workItemId: widget.workItemId,
activityId:
widget.activityId,
workAreaId:
widget.workAreaId,
onSubmit: () {
Navigator.of(context).pop();
},

View File

@ -106,7 +106,8 @@ class TaskActionButtons {
required int completed,
required VoidCallback refreshCallback,
required String parentTaskID,
required String workItemId,
required String activityId,
required String workAreaId,
}) {
return OutlinedButton.icon(
icon: const Icon(Icons.report, size: 18, color: Colors.amber),
@ -131,7 +132,8 @@ class TaskActionButtons {
child: ReportActionBottomSheet(
taskData: taskData,
taskDataId: parentTaskID,
workItemId: workItemId,
workAreaId: workAreaId,
activityId: activityId,
onReportSuccess: refreshCallback,
),
),

View File

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

View File

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