feat: update JobDetailsResponse and JobData models to support nullable fields and improve JSON parsing

This commit is contained in:
Vaibhav Surve 2025-11-20 11:12:59 +05:30
parent 02ef996753
commit c44d10d35a
2 changed files with 181 additions and 162 deletions

View File

@ -1,91 +1,95 @@
class JobDetailsResponse {
final bool success;
final String message;
final bool? success;
final String? message;
final JobData? data;
final dynamic errors;
final int statusCode;
final String timestamp;
final int? statusCode;
final String? timestamp;
JobDetailsResponse({
required this.success,
required this.message,
this.success,
this.message,
this.data,
this.errors,
required this.statusCode,
required this.timestamp,
this.statusCode,
this.timestamp,
});
factory JobDetailsResponse.fromJson(Map<String, dynamic> json) {
factory JobDetailsResponse.fromJson(Map<String, dynamic>? json) {
if (json == null) return JobDetailsResponse();
return JobDetailsResponse(
success: json['success'] as bool,
message: json['message'] as String,
success: json['success'] as bool?,
message: json['message'] as String?,
data: json['data'] != null ? JobData.fromJson(json['data']) : null,
errors: json['errors'],
statusCode: json['statusCode'] as int,
timestamp: json['timestamp'] as String,
statusCode: json['statusCode'] as int?,
timestamp: json['timestamp'] as String?,
);
}
}
class JobData {
final String id;
final String title;
final String description;
final String jobTicketUId;
final Project project;
final List<Assignee> assignees;
final Status status;
final String startDate;
final String dueDate;
final bool isActive;
final String? id;
final String? title;
final String? description;
final String? jobTicketUId;
final Project? project;
final List<Assignee>? assignees;
final Status? status;
final String? startDate;
final String? dueDate;
final bool? isActive;
final dynamic taggingAction;
final String? attendanceId;
final int? nextTaggingAction;
final String createdAt;
final User createdBy;
final List<Tag> tags;
final List<UpdateLog> updateLogs;
final String? createdAt;
final User? createdBy;
final List<Tag>? tags;
final List<UpdateLog>? updateLogs;
JobData({
required this.id,
required this.title,
required this.description,
required this.jobTicketUId,
required this.project,
required this.assignees,
required this.status,
required this.startDate,
required this.dueDate,
required this.isActive,
this.id,
this.title,
this.description,
this.jobTicketUId,
this.project,
this.assignees,
this.status,
this.startDate,
this.dueDate,
this.isActive,
this.taggingAction,
this.attendanceId,
this.nextTaggingAction,
required this.createdAt,
required this.createdBy,
required this.tags,
required this.updateLogs,
this.createdAt,
this.createdBy,
this.tags,
this.updateLogs,
});
factory JobData.fromJson(Map<String, dynamic> json) {
factory JobData.fromJson(Map<String, dynamic>? json) {
if (json == null) return JobData();
return JobData(
id: json['id'] as String,
title: json['title'] as String,
description: json['description'] as String,
jobTicketUId: json['jobTicketUId'] as String,
project: Project.fromJson(json['project']),
id: json['id'] as String?,
title: json['title'] as String?,
description: json['description'] as String?,
jobTicketUId: json['jobTicketUId'] as String?,
project: json['project'] != null ? Project.fromJson(json['project']) : null,
assignees: (json['assignees'] as List<dynamic>?)
?.map((e) => Assignee.fromJson(e))
.toList() ??
[],
status: Status.fromJson(json['status']),
startDate: json['startDate'] as String,
dueDate: json['dueDate'] as String,
isActive: json['isActive'] as bool,
status: json['status'] != null ? Status.fromJson(json['status']) : null,
startDate: json['startDate'] as String?,
dueDate: json['dueDate'] as String?,
isActive: json['isActive'] as bool?,
taggingAction: json['taggingAction'],
attendanceId: json['attendanceId'] as String?,
nextTaggingAction: json['nextTaggingAction'] as int?,
createdAt: json['createdAt'] as String,
createdBy: User.fromJson(json['createdBy']),
createdAt: json['createdAt'] as String?,
createdBy: json['createdBy'] != null ? User.fromJson(json['createdBy']) : null,
tags: (json['tags'] as List<dynamic>?)
?.map((e) => Tag.fromJson(e))
.toList() ??
@ -99,120 +103,128 @@ class JobData {
}
class Project {
final String id;
final String name;
final String shortName;
final String assignedDate;
final String contactName;
final String contactPhone;
final String contactEmail;
final String? id;
final String? name;
final String? shortName;
final String? assignedDate;
final String? contactName;
final String? contactPhone;
final String? contactEmail;
Project({
required this.id,
required this.name,
required this.shortName,
required this.assignedDate,
required this.contactName,
required this.contactPhone,
required this.contactEmail,
this.id,
this.name,
this.shortName,
this.assignedDate,
this.contactName,
this.contactPhone,
this.contactEmail,
});
factory Project.fromJson(Map<String, dynamic> json) {
factory Project.fromJson(Map<String, dynamic>? json) {
if (json == null) return Project();
return Project(
id: json['id'] as String,
name: json['name'] as String,
shortName: json['shortName'] as String,
assignedDate: json['assignedDate'] as String,
contactName: json['contactName'] as String,
contactPhone: json['contactPhone'] as String,
contactEmail: json['contactEmail'] as String,
id: json['id'] as String?,
name: json['name'] as String?,
shortName: json['shortName'] as String?,
assignedDate: json['assignedDate'] as String?,
contactName: json['contactName'] as String?,
contactPhone: json['contactPhone'] as String?,
contactEmail: json['contactEmail'] as String?,
);
}
}
class Assignee {
final String id;
final String firstName;
final String lastName;
final String? id;
final String? firstName;
final String? lastName;
final String? email;
final String? photo;
final String jobRoleId;
final String jobRoleName;
final String? jobRoleId;
final String? jobRoleName;
Assignee({
required this.id,
required this.firstName,
required this.lastName,
this.id,
this.firstName,
this.lastName,
this.email,
this.photo,
required this.jobRoleId,
required this.jobRoleName,
this.jobRoleId,
this.jobRoleName,
});
factory Assignee.fromJson(Map<String, dynamic> json) {
factory Assignee.fromJson(Map<String, dynamic>? json) {
if (json == null) return Assignee();
return Assignee(
id: json['id'] as String,
firstName: json['firstName'] as String,
lastName: json['lastName'] as String,
id: json['id'] as String?,
firstName: json['firstName'] as String?,
lastName: json['lastName'] as String?,
email: json['email'] as String?,
photo: json['photo'] as String?,
jobRoleId: json['jobRoleId'] as String,
jobRoleName: json['jobRoleName'] as String,
jobRoleId: json['jobRoleId'] as String?,
jobRoleName: json['jobRoleName'] as String?,
);
}
}
class Status {
final String id;
final String name;
final String displayName;
final int level;
final String? id;
final String? name;
final String? displayName;
final int? level;
Status({
required this.id,
required this.name,
required this.displayName,
required this.level,
this.id,
this.name,
this.displayName,
this.level,
});
factory Status.fromJson(Map<String, dynamic> json) {
factory Status.fromJson(Map<String, dynamic>? json) {
if (json == null) return Status();
return Status(
id: json['id'] as String,
name: json['name'] as String,
displayName: json['displayName'] as String,
level: json['level'] as int,
id: json['id'] as String?,
name: json['name'] as String?,
displayName: json['displayName'] as String?,
level: json['level'] as int?,
);
}
}
class User {
final String id;
final String firstName;
final String lastName;
final String? id;
final String? firstName;
final String? lastName;
final String? email;
final String? photo;
final String jobRoleId;
final String jobRoleName;
final String? jobRoleId;
final String? jobRoleName;
User({
required this.id,
required this.firstName,
required this.lastName,
this.id,
this.firstName,
this.lastName,
this.email,
this.photo,
required this.jobRoleId,
required this.jobRoleName,
this.jobRoleId,
this.jobRoleName,
});
factory User.fromJson(Map<String, dynamic> json) {
factory User.fromJson(Map<String, dynamic>? json) {
if (json == null) return User();
return User(
id: json['id'] as String,
firstName: json['firstName'] as String,
lastName: json['lastName'] as String,
id: json['id'] as String?,
firstName: json['firstName'] as String?,
lastName: json['lastName'] as String?,
email: json['email'] as String?,
photo: json['photo'] as String?,
jobRoleId: json['jobRoleId'] as String,
jobRoleName: json['jobRoleName'] as String,
jobRoleId: json['jobRoleId'] as String?,
jobRoleName: json['jobRoleName'] as String?,
);
}
}
@ -223,7 +235,9 @@ class Tag {
Tag({this.id, this.name});
factory Tag.fromJson(Map<String, dynamic> json) {
factory Tag.fromJson(Map<String, dynamic>? json) {
if (json == null) return Tag();
return Tag(
id: json['id'] as String?,
name: json['name'] as String?,
@ -232,27 +246,29 @@ class Tag {
}
class UpdateLog {
final String id;
final String? id;
final Status? status;
final Status nextStatus;
final String comment;
final User updatedBy;
final Status? nextStatus;
final String? comment;
final User? updatedBy;
UpdateLog({
required this.id,
this.id,
this.status,
required this.nextStatus,
required this.comment,
required this.updatedBy,
this.nextStatus,
this.comment,
this.updatedBy,
});
factory UpdateLog.fromJson(Map<String, dynamic> json) {
factory UpdateLog.fromJson(Map<String, dynamic>? json) {
if (json == null) return UpdateLog();
return UpdateLog(
id: json['id'] as String,
id: json['id'] as String?,
status: json['status'] != null ? Status.fromJson(json['status']) : null,
nextStatus: Status.fromJson(json['nextStatus']),
comment: json['comment'] as String,
updatedBy: User.fromJson(json['updatedBy']),
nextStatus: json['nextStatus'] != null ? Status.fromJson(json['nextStatus']) : null,
comment: json['comment'] as String?,
updatedBy: json['updatedBy'] != null ? User.fromJson(json['updatedBy']) : null,
);
}
}

View File

@ -50,15 +50,16 @@ class _JobDetailsScreenState extends State<JobDetailsScreen> with UIMixin {
controller.fetchJobDetail(widget.jobId).then((_) {
final job = controller.jobDetail.value?.data;
if (job != null) {
_titleController.text = job.title;
_descriptionController.text = job.description;
_titleController.text = job.title ?? '';
_descriptionController.text = job.description ?? '';
_startDateController.text = DateTimeUtils.convertUtcToLocal(
job.startDate,
job.startDate ?? DateTime.now().toIso8601String(),
format: "yyyy-MM-dd");
_dueDateController.text =
DateTimeUtils.convertUtcToLocal(job.dueDate, format: "yyyy-MM-dd");
_selectedAssignees.value = job.assignees;
_selectedTags.value = job.tags;
_dueDateController.text = DateTimeUtils.convertUtcToLocal(
job.dueDate ?? '',
format: "yyyy-MM-dd");
_selectedAssignees.value = job.assignees ?? [];
_selectedTags.value = job.tags ?? [];
}
});
}
@ -114,14 +115,14 @@ class _JobDetailsScreenState extends State<JobDetailsScreen> with UIMixin {
}
final originalAssignees = job.assignees;
final assigneesPayload = originalAssignees.map((a) {
final assigneesPayload = originalAssignees?.map((a) {
final isSelected = _selectedAssignees.any((s) => s.id == a.id);
return {"employeeId": a.id, "isActive": isSelected};
}).toList();
for (var s in _selectedAssignees) {
if (!originalAssignees.any((a) => a.id == s.id)) {
assigneesPayload.add({"employeeId": s.id, "isActive": true});
if (!(originalAssignees?.any((a) => a.id == s.id) ?? false)) {
assigneesPayload?.add({"employeeId": s.id, "isActive": true});
}
}
@ -129,7 +130,7 @@ class _JobDetailsScreenState extends State<JobDetailsScreen> with UIMixin {
{"op": "replace", "path": "/assignees", "value": assigneesPayload});
final originalTags = job.tags;
final replaceTagsPayload = originalTags.map((t) {
final replaceTagsPayload = originalTags?.map((t) {
final isSelected = _selectedTags.any((s) => s.id == t.id);
return {"id": t.id, "name": t.name, "isActive": isSelected};
}).toList();
@ -139,7 +140,7 @@ class _JobDetailsScreenState extends State<JobDetailsScreen> with UIMixin {
.map((t) => {"name": t.name, "isActive": true})
.toList();
if (replaceTagsPayload.isNotEmpty) {
if ((replaceTagsPayload?.isNotEmpty ?? false)) {
operations
.add({"op": "replace", "path": "/tags", "value": replaceTagsPayload});
}
@ -157,7 +158,7 @@ class _JobDetailsScreenState extends State<JobDetailsScreen> with UIMixin {
}
final success = await ApiService.editServiceProjectJobApi(
jobId: job.id,
jobId: job.id ?? "",
operations: operations,
);
@ -207,7 +208,7 @@ class _JobDetailsScreenState extends State<JobDetailsScreen> with UIMixin {
);
await controller.updateJobAttendance(
jobId: job.id,
jobId: job.id ?? "",
action: action == 0 ? 0 : 1,
comment: comment,
attachment: attachmentFile,
@ -352,14 +353,14 @@ class _JobDetailsScreenState extends State<JobDetailsScreen> with UIMixin {
onTap: () async {
final initiallySelected = assignees.map<EmployeeModel>((a) {
return EmployeeModel(
id: a.id,
employeeId: a.id,
firstName: a.firstName,
lastName: a.lastName,
id: a.id ?? '',
employeeId: a.id ?? '',
firstName: a.firstName ?? '',
lastName: a.lastName ?? '',
name: "${a.firstName} ${a.lastName}",
designation: a.jobRoleName,
jobRole: a.jobRoleName,
jobRoleID: a.jobRoleId,
designation: a.jobRoleName ?? '',
jobRole: a.jobRoleName ?? '',
jobRoleID: a.jobRoleId ?? '',
email: a.email ?? '',
phoneNumber: '',
activity: 0,
@ -495,7 +496,7 @@ class _JobDetailsScreenState extends State<JobDetailsScreen> with UIMixin {
onPressed: () async {
isAttendanceExpanded.value =
!isAttendanceExpanded.value;
if (isAttendanceExpanded.value && job != null) {
if (isAttendanceExpanded.value ) {
await controller
.fetchJobAttendanceLog(job.attendanceId ?? '');
}
@ -774,11 +775,11 @@ class _JobDetailsScreenState extends State<JobDetailsScreen> with UIMixin {
titleIcon: Icons.label_outline,
children: [_tagEditor()]),
MySpacing.height(16),
if (job.updateLogs.isNotEmpty)
if ((job.updateLogs?.isNotEmpty ?? false))
_buildSectionCard(
title: "Update Logs",
titleIcon: Icons.history,
children: [JobTimeline(logs: job.updateLogs)]),
children: [JobTimeline(logs: job.updateLogs ?? [])]),
MySpacing.height(80),
],
),
@ -806,12 +807,14 @@ class JobTimeline extends StatelessWidget {
itemBuilder: (_, index) {
final log = reversedLogs[index];
final statusName = log.status?.displayName ?? "Created";
final nextStatusName = log.nextStatus.displayName;
final comment = log.comment;
final nextStatusName = log.nextStatus?.displayName ?? "N/A";
final comment = log.comment ?? '';
final updatedBy =
"${log.updatedBy.firstName} ${log.updatedBy.lastName}";
"${log.updatedBy?.firstName ?? ''} ${log.updatedBy?.lastName ?? ''}";
final f = log.updatedBy?.firstName ?? '';
final l = log.updatedBy?.lastName ?? '';
final initials =
"${log.updatedBy.firstName.isNotEmpty ? log.updatedBy.firstName[0] : ''}${log.updatedBy.lastName.isNotEmpty ? log.updatedBy.lastName[0] : ''}";
"${f.isNotEmpty ? f[0] : ''}${l.isNotEmpty ? l[0] : ''}";
return TimelineTile(
alignment: TimelineAlign.start,