Add comment task feature with API integration and UI updates

This commit is contained in:
Vaibhav Surve 2025-05-13 17:02:31 +05:30
parent 42935471cf
commit 628aaa0387
6 changed files with 257 additions and 62 deletions

View File

@ -39,7 +39,7 @@ class ReportTaskController extends MyController {
label: "Team Size",
controller: TextEditingController(),
);
basicValidator.addField(
basicValidator.addField(
'task_id',
label: "Task Id",
controller: TextEditingController(),
@ -66,6 +66,16 @@ class ReportTaskController extends MyController {
label: "Assigned By",
controller: TextEditingController(),
);
basicValidator.addField(
'team_members',
label: "Team Members",
controller: TextEditingController(),
);
basicValidator.addField(
'planned_work',
label: "Planned Work",
controller: TextEditingController(),
);
logger.i(
"Fields initialized for assigned_date, work_area, activity, team_size, assigned, completed_work, and comment.");
@ -95,7 +105,7 @@ class ReportTaskController extends MyController {
}
try {
isLoading.value = true;
isLoading.value = true;
final success = await ApiService.reportTask(
id: projectId,
@ -113,7 +123,50 @@ class ReportTaskController extends MyController {
logger.e("Error reporting task: $e");
Get.snackbar("Error", "An error occurred while reporting the task.");
} finally {
isLoading.value = false;
isLoading.value = false;
}
}
Future<void> commentTask({
required String projectId,
required String comment,
required int completedTask,
required List<Map<String, dynamic>> checklist,
required DateTime reportedDate,
}) async {
logger.i("Starting task report...");
final completedWork =
basicValidator.getController('completed_work')?.text.trim();
final commentField = basicValidator.getController('comment')?.text.trim();
if (completedWork == null || completedWork.isEmpty) {
Get.snackbar("Error", "Completed work is required.");
return;
}
if (commentField == null || commentField.isEmpty) {
Get.snackbar("Error", "Comment is required.");
return;
}
try {
isLoading.value = true;
final success = await ApiService.commentTask(
id: projectId,
comment: commentField,
);
if (success) {
Get.snackbar("Success", "Task reported successfully!");
} else {
Get.snackbar("Error", "Failed to report task.");
}
} catch (e) {
logger.e("Error reporting task: $e");
Get.snackbar("Error", "An error occurred while reporting the task.");
} finally {
isLoading.value = false;
}
}
}

View File

@ -18,4 +18,5 @@ class ApiEndpoints {
// Daily Task Screen API Endpoints
static const String getDailyTask = "/task/list";
static const String reportTask = "/task/report";
static const String commentTask = "task/comment";
}

View File

@ -331,6 +331,32 @@ class ApiService {
final json = jsonDecode(response.body);
if (response.statusCode == 200 && json['success'] == true) {
return true;
} else {
_log("Failed to report task: ${json['message'] ?? 'Unknown error'}");
return false;
}
}
static Future<bool> commentTask({
required String id,
required String comment,
}) async {
final body = {
"taskAllocationId": id,
"comment": comment,
"commentDate": DateTime.now().toUtc().toIso8601String(),
};
final response = await _postRequest(ApiEndpoints.commentTask, body);
if (response == null) {
_log("Error: No response from server.");
return false;
}
final json = jsonDecode(response.body);
if (response.statusCode == 200 && json['success'] == true) {
return true;
} else {

View File

@ -40,9 +40,8 @@ class TaskModel {
teamMembers: (json['teamMembers'] as List)
.map((e) => TeamMember.fromJson(e))
.toList(),
comments: (json['comments'] as List)
.map((e) => Comment.fromJson(e))
.toList(),
comments:
(json['comments'] as List).map((e) => Comment.fromJson(e)).toList(),
);
}
}
@ -65,9 +64,8 @@ class WorkItem {
activityMaster: json['activityMaster'] != null
? ActivityMaster.fromJson(json['activityMaster'])
: null,
workArea: json['workArea'] != null
? WorkArea.fromJson(json['workArea'])
: null,
workArea:
json['workArea'] != null ? WorkArea.fromJson(json['workArea']) : null,
plannedWork: json['plannedWork'],
completedWork: json['completedWork'],
);
@ -139,20 +137,34 @@ class AssignedBy {
class TeamMember {
final String firstName;
final String? lastName;
TeamMember({required this.firstName});
TeamMember({required this.firstName, this.lastName});
factory TeamMember.fromJson(Map<String, dynamic> json) {
return TeamMember(firstName: json['firstName']);
return TeamMember(
firstName: json['firstName'],
lastName: json['lastName'],
);
}
}
class Comment {
final String comment;
final TeamMember commentedBy;
final String timestamp;
Comment({required this.comment});
Comment({
required this.comment,
required this.commentedBy,
required this.timestamp,
});
factory Comment.fromJson(Map<String, dynamic> json) {
return Comment(comment: json['comment']);
return Comment(
comment: json['comment'],
commentedBy: TeamMember.fromJson(json['employee']),
timestamp: json['commentDate'],
);
}
}

View File

@ -245,16 +245,17 @@ class _DailyTaskScreenState extends State<DailyTaskScreen> with UIMixin {
return DataRow(cells: [
DataCell(
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
MyText.bodyMedium(workItem?.activityMaster?.activityName ?? 'N/A', fontWeight: 600),
SizedBox(height: 2),
MyText.bodySmall(location, color: Colors.grey),
],
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
MyText.bodyMedium(workItem?.activityMaster?.activityName ?? 'N/A',
fontWeight: 600),
SizedBox(height: 2),
MyText.bodySmall(location, color: Colors.grey),
],
),
),
DataCell(
MyText.bodyMedium(
'${task.plannedTask ?? "NA"} / '
@ -313,7 +314,44 @@ class _DailyTaskScreenState extends State<DailyTaskScreen> with UIMixin {
),
const SizedBox(width: 8),
ElevatedButton(
onPressed: () {},
onPressed: () {
final activityName =
task.workItem?.activityMaster?.activityName ?? 'N/A';
final assigned = '${task.plannedTask ?? "NA"} / '
'${(task.workItem?.plannedWork != null && task.workItem?.completedWork != null) ? (task.workItem!.plannedWork! - task.workItem.completedWork!) : "NA"}';
final plannedWork = '${(task.plannedTask.toString())}';
final assignedBy =
"${task.assignedBy.firstName} ${task.assignedBy.lastName ?? ''}";
final completedWork = '${(task.completedTask.toString())}';
final assignedOn = DateFormat('dd-MM-yyyy')
.format(DateTime.parse(task.assignmentDate));
final taskId = task.id;
final location = [
task.workItem?.workArea?.floor?.building?.name,
task.workItem?.workArea?.floor?.floorName,
task.workItem?.workArea?.areaName
].where((e) => e != null && e.isNotEmpty).join(' > ');
final teamMembers =
task.teamMembers.map((member) => member.firstName).toList();
final taskComments = task.comments.map((comment) => comment.comment ?? 'No Content').toList();
Get.toNamed(
'/daily-task/comment-task',
arguments: {
'activity': activityName,
'assigned': assigned,
'taskId': taskId,
'assignedBy': assignedBy,
'completedWork': completedWork,
'plannedWork': plannedWork,
'assignedOn': assignedOn,
'location': location,
'teamSize': task.teamMembers.length,
'teamMembers': teamMembers,
'taskComments': taskComments
},
);
},
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 6),
minimumSize: const Size(60, 20),

View File

@ -15,6 +15,7 @@ import 'package:marco/helpers/widgets/my_spacing.dart';
import 'package:marco/helpers/widgets/my_text.dart';
import 'package:marco/helpers/widgets/my_text_style.dart';
import 'package:marco/view/layouts/layout.dart';
import 'package:marco/helpers/widgets/avatar.dart';
class CommentTaskScreen extends StatefulWidget {
const CommentTaskScreen({super.key});
@ -38,8 +39,12 @@ class _CommentTaskScreenState extends State<CommentTaskScreen> with UIMixin {
taskData['location'] ?? '';
controller.basicValidator.getController('activity')?.text =
taskData['activity'] ?? '';
controller.basicValidator.getController('team_size')?.text =
taskData['teamSize'].toString();
controller.basicValidator.getController('planned_work')?.text =
taskData['plannedWork'] ?? '';
controller.basicValidator.getController('completed_work')?.text =
taskData['completedWork'] ?? '';
controller.basicValidator.getController('team_members')?.text =
(taskData['teamMembers'] as List<dynamic>).join(', ');
controller.basicValidator.getController('assigned')?.text =
taskData['assigned'] ?? '';
controller.basicValidator.getController('task_id')?.text =
@ -48,7 +53,7 @@ class _CommentTaskScreenState extends State<CommentTaskScreen> with UIMixin {
return Layout(
child: GetBuilder<ReportTaskController>(
init: controller,
tag: 'report_task_controller',
tag: 'comment_task_controller',
builder: (controller) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
@ -58,12 +63,12 @@ class _CommentTaskScreenState extends State<CommentTaskScreen> with UIMixin {
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
MyText.titleMedium("Report Task",
MyText.titleMedium("Comment Task",
fontSize: 18, fontWeight: 600),
MyBreadcrumb(
children: [
MyBreadcrumbItem(name: 'Daily Task'),
MyBreadcrumbItem(name: 'Report Task'),
MyBreadcrumbItem(name: 'Comment Task'),
],
),
],
@ -100,18 +105,11 @@ class _CommentTaskScreenState extends State<CommentTaskScreen> with UIMixin {
children: [
Icon(LucideIcons.server, size: 16),
MySpacing.width(12),
MyText.titleMedium("General", fontWeight: 600),
MyText.titleMedium("Activity Summary", fontWeight: 600),
],
),
MySpacing.height(24),
// Static fields
buildRow(
"Assigned Date",
controller.basicValidator
.getController('assigned_date')
?.text
.trim()),
buildRow(
"Assigned By",
controller.basicValidator
@ -131,39 +129,19 @@ class _CommentTaskScreenState extends State<CommentTaskScreen> with UIMixin {
?.text
.trim()),
buildRow(
"Team Size",
"Planned Work",
controller.basicValidator
.getController('team_size')
.getController('planned_work')
?.text
.trim()),
buildRow(
"Assigned",
"Completed Work",
controller.basicValidator
.getController('assigned')
.getController('completed_work')
?.text
.trim()),
buildTeamMembers(),
// Input fields
MyText.labelMedium("Completed Work"),
MySpacing.height(8),
TextFormField(
validator:
controller.basicValidator.getValidation('completed_work'),
controller:
controller.basicValidator.getController('completed_work'),
keyboardType: TextInputType.number,
decoration: InputDecoration(
hintText: "eg: 10",
hintStyle: MyTextStyle.bodySmall(xMuted: true),
border: outlineInputBorder,
enabledBorder: outlineInputBorder,
focusedBorder: focusedInputBorder,
contentPadding: MySpacing.all(16),
isCollapsed: true,
floatingLabelBehavior: FloatingLabelBehavior.never,
),
),
MySpacing.height(24),
MyText.labelMedium("Comment"),
MySpacing.height(8),
TextFormField(
@ -221,7 +199,7 @@ class _CommentTaskScreenState extends State<CommentTaskScreen> with UIMixin {
backgroundColor: contentTheme.primary,
borderRadiusAll: AppStyle.buttonRadius.medium,
child: MyText.bodySmall(
'Save',
'Comment',
color: contentTheme.onPrimary,
),
),
@ -239,6 +217,93 @@ class _CommentTaskScreenState extends State<CommentTaskScreen> with UIMixin {
),
);
}
Widget buildTeamMembers() {
final teamMembersText =
controller.basicValidator.getController('team_members')?.text ?? '';
final members = teamMembersText
.split(',')
.map((e) => e.trim())
.where((e) => e.isNotEmpty)
.toList();
return Padding(
padding: const EdgeInsets.only(bottom: 16),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
MyText.labelMedium("Team Members:"),
MySpacing.width(12),
GestureDetector(
onTap: () => showTeamMembersDialog(members),
child: SizedBox(
height: 32,
width: 100,
child: Stack(
children: [
for (int i = 0; i < members.length.clamp(0, 3); i++)
Positioned(
left: i * 24.0,
child: Tooltip(
message: members[i],
child: Avatar(
firstName: members[i],
lastName: '',
size: 32,
),
),
),
if (members.length > 3)
Positioned(
left: 2 * 24.0,
child: CircleAvatar(
radius: 16,
backgroundColor: Colors.grey.shade300,
child: MyText.bodyMedium(
'+${members.length - 3}',
style: const TextStyle(
fontSize: 12, color: Colors.black87),
),
),
),
],
),
),
),
],
),
);
}
void showTeamMembersDialog(List<String> members) {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: MyText.titleMedium('Team Members'),
content: SizedBox(
width: 300,
child: ListView.builder(
shrinkWrap: true,
itemCount: members.length,
itemBuilder: (context, index) {
final name = members[index];
return ListTile(
leading: Avatar(firstName: name, lastName: "", size: 32),
title: MyText.bodyMedium(name),
);
},
),
),
actions: [
TextButton(
onPressed: () => Get.back(),
child: MyText.bodySmall("Close"),
),
],
);
},
);
}
Widget buildRow(String label, String? value) {
print("Label: $label, Value: $value");