feat: Implement task management features including task creation, assignment, and employee selection
This commit is contained in:
parent
93a2350858
commit
206c84b3a1
202
lib/controller/task_planing/add_task_controller.dart
Normal file
202
lib/controller/task_planing/add_task_controller.dart
Normal 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();
|
||||
}
|
||||
}
|
@ -26,4 +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 getmasterWorkCategories = "/Master/work-categories";
|
||||
}
|
||||
|
@ -378,6 +378,11 @@ class ApiService {
|
||||
_getRequest(ApiEndpoints.getWorkStatus).then((res) => res != null
|
||||
? _parseResponseForAllData(res, label: 'Work Status')
|
||||
: 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({
|
||||
required String id,
|
||||
required String comment,
|
||||
@ -399,4 +404,35 @@ class ApiService {
|
||||
final json = jsonDecode(response.body);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -1,20 +1,92 @@
|
||||
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:marco/helpers/widgets/my_snackbar.dart';
|
||||
|
||||
void showManageTaskBottomSheet({
|
||||
required String building,
|
||||
required String floor,
|
||||
required String workArea,
|
||||
required String activity,
|
||||
required String plannedWork,
|
||||
required String completedWork,
|
||||
required String unit,
|
||||
required Function(String) onCategoryChanged,
|
||||
required String parentTaskId,
|
||||
required int plannedTask,
|
||||
required String workItemId,
|
||||
required VoidCallback onSubmit,
|
||||
}) {
|
||||
final List<String> categories = ["Fresh Work", "Repair", "Demo"];
|
||||
String selectedCategory = 'Fresh Work';
|
||||
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(
|
||||
@ -22,12 +94,14 @@ void showManageTaskBottomSheet({
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
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(
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(maxHeight: 0.95 * 800),
|
||||
padding: EdgeInsets.fromLTRB(horizontalPadding, 12, horizontalPadding, 24),
|
||||
constraints: const BoxConstraints(maxHeight: 760),
|
||||
padding: EdgeInsets.fromLTRB(
|
||||
horizontalPadding, 12, horizontalPadding, 24),
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
||||
@ -36,7 +110,6 @@ void showManageTaskBottomSheet({
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Draggable handle
|
||||
Center(
|
||||
child: Container(
|
||||
width: 40,
|
||||
@ -48,74 +121,155 @@ void showManageTaskBottomSheet({
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
_infoRowWithIcon(Icons.location_city, "Selected Building", building),
|
||||
_infoRowWithIcon(Icons.apartment, "Selected Floor", floor),
|
||||
_infoRowWithIcon(Icons.workspaces, "Selected Work Area", workArea),
|
||||
_infoRowWithIcon(Icons.list_alt, "Selected Activity", activity),
|
||||
|
||||
_infoRowWithIcon(
|
||||
Icons.workspaces, "Selected Work Area", workArea),
|
||||
_infoRowWithIcon(
|
||||
Icons.list_alt, "Selected Activity", activity),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
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),
|
||||
MyText.bodyMedium("Selected Work Category", fontWeight: 600),
|
||||
MyText.bodyMedium("Planned Work", fontWeight: 600),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
|
||||
Container(
|
||||
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) {
|
||||
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),
|
||||
],
|
||||
),
|
||||
TextField(
|
||||
controller: plannedTaskController,
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: InputDecoration(
|
||||
hintText: "Enter planned work",
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8)),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 12, vertical: 10),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 12),
|
||||
_infoRowWithIcon(Icons.edit_calendar, "Planned Work", plannedWork),
|
||||
_infoRowWithIcon(Icons.check_circle_outline, "Completed Work", completedWork),
|
||||
_infoRowWithIcon(Icons.straighten, "Unit", unit),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: 48,
|
||||
child: ElevatedButton(
|
||||
onPressed: onSubmit,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.blueAccent,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||||
),
|
||||
child: const Text(
|
||||
"Submit",
|
||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
|
||||
),
|
||||
_infoRowWithIcon(Icons.check_circle_outline,
|
||||
"Completed Work", completedWork),
|
||||
const SizedBox(height: 16),
|
||||
MyText.bodyMedium("Description", fontWeight: 700),
|
||||
const SizedBox(height: 6),
|
||||
TextField(
|
||||
controller: descriptionController,
|
||||
maxLines: 3,
|
||||
decoration: InputDecoration(
|
||||
hintText: "Enter task description",
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8)),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 12, vertical: 10),
|
||||
),
|
||||
),
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
31
lib/model/dailyTaskPlaning/master_work_category_model.dart
Normal file
31
lib/model/dailyTaskPlaning/master_work_category_model.dart
Normal 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,
|
||||
};
|
||||
}
|
||||
}
|
@ -17,11 +17,16 @@ import 'dart:io';
|
||||
class ReportActionBottomSheet extends StatefulWidget {
|
||||
final Map<String, dynamic> taskData;
|
||||
final VoidCallback? onCommentSuccess;
|
||||
|
||||
final String taskDataId;
|
||||
final String workItemId;
|
||||
final VoidCallback onReportSuccess;
|
||||
const ReportActionBottomSheet({
|
||||
super.key,
|
||||
required this.taskData,
|
||||
this.onCommentSuccess,
|
||||
required this.taskDataId,
|
||||
required this.workItemId,
|
||||
required this.onReportSuccess,
|
||||
});
|
||||
|
||||
@override
|
||||
@ -70,7 +75,10 @@ class _ReportActionBottomSheetState extends State<ReportActionBottomSheet>
|
||||
controller.basicValidator.getController('task_id')?.text =
|
||||
data['taskId'] ?? '';
|
||||
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((_) {
|
||||
@ -461,16 +469,11 @@ class _ReportActionBottomSheetState extends State<ReportActionBottomSheet>
|
||||
if (success) {
|
||||
Navigator.of(context).pop();
|
||||
if (shouldShowAddTaskSheet) {
|
||||
await Future.delayed(Duration(
|
||||
milliseconds:
|
||||
100)); // Ensure animation completes
|
||||
await Future.delayed(
|
||||
Duration(milliseconds: 100));
|
||||
showManageTaskBottomSheet(
|
||||
building: widget.taskData['building'] ?? '',
|
||||
floor: widget.taskData['floor'] ?? '',
|
||||
workArea: widget.taskData['location'] ?? '',
|
||||
activity: widget.taskData['activity'] ?? '',
|
||||
plannedWork:
|
||||
widget.taskData['plannedWork'] ?? '',
|
||||
completedWork:
|
||||
widget.taskData['completedWork'] ?? '',
|
||||
unit: widget.taskData['unit'] ?? '',
|
||||
@ -478,15 +481,18 @@ class _ReportActionBottomSheetState extends State<ReportActionBottomSheet>
|
||||
debugPrint(
|
||||
"Category changed to: $category");
|
||||
},
|
||||
parentTaskId: widget.taskDataId,
|
||||
plannedTask: int.tryParse(
|
||||
widget.taskData['plannedWork'] ??
|
||||
'0') ??
|
||||
0,
|
||||
workItemId: widget.workItemId,
|
||||
onSubmit: () {
|
||||
Navigator.of(context)
|
||||
.pop(); // Close second sheet
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Optional callback after comment success
|
||||
widget.onCommentSuccess?.call();
|
||||
widget.onReportSuccess.call();
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -82,7 +82,8 @@ class TaskActionButtons {
|
||||
textStyle: const TextStyle(fontSize: 14),
|
||||
),
|
||||
onPressed: () {
|
||||
final taskData = _prepareTaskData(task: task, completed: task.completedTask);
|
||||
final taskData =
|
||||
_prepareTaskData(task: task, completed: task.completedTask);
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
@ -104,6 +105,8 @@ class TaskActionButtons {
|
||||
required dynamic task,
|
||||
required int completed,
|
||||
required VoidCallback refreshCallback,
|
||||
required String parentTaskID,
|
||||
required String workItemId,
|
||||
}) {
|
||||
return OutlinedButton.icon(
|
||||
icon: const Icon(Icons.report, size: 18, color: Colors.amber),
|
||||
@ -115,7 +118,7 @@ class TaskActionButtons {
|
||||
textStyle: const TextStyle(fontSize: 14),
|
||||
),
|
||||
onPressed: () {
|
||||
final taskData = _prepareTaskData(task: task, completed: completed);
|
||||
final taskData = _prepareTaskData(task: task, completed: completed);
|
||||
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
@ -127,6 +130,9 @@ class TaskActionButtons {
|
||||
padding: MediaQuery.of(ctx).viewInsets,
|
||||
child: ReportActionBottomSheet(
|
||||
taskData: taskData,
|
||||
taskDataId: parentTaskID,
|
||||
workItemId: workItemId,
|
||||
onReportSuccess: refreshCallback,
|
||||
),
|
||||
),
|
||||
);
|
||||
@ -135,61 +141,61 @@ class TaskActionButtons {
|
||||
}
|
||||
|
||||
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;
|
||||
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 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 teamMembers = task.teamMembers
|
||||
.map((e) => '${e.firstName} ${e.lastName ?? ''}')
|
||||
.toList();
|
||||
|
||||
final pendingWork =
|
||||
(task.workItem?.plannedWork ?? 0) - (task.workItem?.completedWork ?? 0);
|
||||
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";
|
||||
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 {
|
||||
'text': comment.comment,
|
||||
'date': isoDate,
|
||||
'commentedBy': commenterName,
|
||||
'preSignedUrls': comment.preSignedUrls,
|
||||
'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,
|
||||
};
|
||||
}).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,
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -379,7 +379,8 @@ class _DailyProgressReportScreenState extends State<DailyProgressReportScreen>
|
||||
final progress = (planned != 0)
|
||||
? (completed / planned).clamp(0.0, 1.0)
|
||||
: 0.0;
|
||||
|
||||
final parentTaskID = task.id;
|
||||
final workItemId = task.workItem?.id;
|
||||
return Column(
|
||||
children: [
|
||||
Padding(
|
||||
@ -472,6 +473,8 @@ class _DailyProgressReportScreenState extends State<DailyProgressReportScreen>
|
||||
TaskActionButtons.reportActionButton(
|
||||
context: context,
|
||||
task: task,
|
||||
parentTaskID: parentTaskID,
|
||||
workItemId: workItemId.toString(),
|
||||
completed: completed,
|
||||
refreshCallback: _refreshData,
|
||||
),
|
||||
|
@ -98,8 +98,6 @@ flutter:
|
||||
- assets/avatar/
|
||||
- assets/coin/
|
||||
- assets/country/
|
||||
- assets/data/
|
||||
- assets/dummy/
|
||||
- assets/lang/
|
||||
- assets/logo/
|
||||
- assets/logo/loading_logo.png
|
||||
|
Loading…
x
Reference in New Issue
Block a user