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 { class JobDetailsResponse {
final bool success; final bool? success;
final String message; final String? message;
final JobData? data; final JobData? data;
final dynamic errors; final dynamic errors;
final int statusCode; final int? statusCode;
final String timestamp; final String? timestamp;
JobDetailsResponse({ JobDetailsResponse({
required this.success, this.success,
required this.message, this.message,
this.data, this.data,
this.errors, this.errors,
required this.statusCode, this.statusCode,
required this.timestamp, this.timestamp,
}); });
factory JobDetailsResponse.fromJson(Map<String, dynamic> json) { factory JobDetailsResponse.fromJson(Map<String, dynamic>? json) {
if (json == null) return JobDetailsResponse();
return JobDetailsResponse( return JobDetailsResponse(
success: json['success'] as bool, success: json['success'] as bool?,
message: json['message'] as String, message: json['message'] as String?,
data: json['data'] != null ? JobData.fromJson(json['data']) : null, data: json['data'] != null ? JobData.fromJson(json['data']) : null,
errors: json['errors'], errors: json['errors'],
statusCode: json['statusCode'] as int, statusCode: json['statusCode'] as int?,
timestamp: json['timestamp'] as String, timestamp: json['timestamp'] as String?,
); );
} }
} }
class JobData { class JobData {
final String id; final String? id;
final String title; final String? title;
final String description; final String? description;
final String jobTicketUId; final String? jobTicketUId;
final Project project; final Project? project;
final List<Assignee> assignees; final List<Assignee>? assignees;
final Status status; final Status? status;
final String startDate; final String? startDate;
final String dueDate; final String? dueDate;
final bool isActive; final bool? isActive;
final dynamic taggingAction; final dynamic taggingAction;
final String? attendanceId; final String? attendanceId;
final int? nextTaggingAction; final int? nextTaggingAction;
final String createdAt; final String? createdAt;
final User createdBy; final User? createdBy;
final List<Tag> tags; final List<Tag>? tags;
final List<UpdateLog> updateLogs; final List<UpdateLog>? updateLogs;
JobData({ JobData({
required this.id, this.id,
required this.title, this.title,
required this.description, this.description,
required this.jobTicketUId, this.jobTicketUId,
required this.project, this.project,
required this.assignees, this.assignees,
required this.status, this.status,
required this.startDate, this.startDate,
required this.dueDate, this.dueDate,
required this.isActive, this.isActive,
this.taggingAction, this.taggingAction,
this.attendanceId, this.attendanceId,
this.nextTaggingAction, this.nextTaggingAction,
required this.createdAt, this.createdAt,
required this.createdBy, this.createdBy,
required this.tags, this.tags,
required this.updateLogs, this.updateLogs,
}); });
factory JobData.fromJson(Map<String, dynamic> json) { factory JobData.fromJson(Map<String, dynamic>? json) {
if (json == null) return JobData();
return JobData( return JobData(
id: json['id'] as String, id: json['id'] as String?,
title: json['title'] as String, title: json['title'] as String?,
description: json['description'] as String, description: json['description'] as String?,
jobTicketUId: json['jobTicketUId'] as String, jobTicketUId: json['jobTicketUId'] as String?,
project: Project.fromJson(json['project']), project: json['project'] != null ? Project.fromJson(json['project']) : null,
assignees: (json['assignees'] as List<dynamic>?) assignees: (json['assignees'] as List<dynamic>?)
?.map((e) => Assignee.fromJson(e)) ?.map((e) => Assignee.fromJson(e))
.toList() ?? .toList() ??
[], [],
status: Status.fromJson(json['status']), status: json['status'] != null ? Status.fromJson(json['status']) : null,
startDate: json['startDate'] as String, startDate: json['startDate'] as String?,
dueDate: json['dueDate'] as String, dueDate: json['dueDate'] as String?,
isActive: json['isActive'] as bool, isActive: json['isActive'] as bool?,
taggingAction: json['taggingAction'], taggingAction: json['taggingAction'],
attendanceId: json['attendanceId'] as String?, attendanceId: json['attendanceId'] as String?,
nextTaggingAction: json['nextTaggingAction'] as int?, nextTaggingAction: json['nextTaggingAction'] as int?,
createdAt: json['createdAt'] as String, createdAt: json['createdAt'] as String?,
createdBy: User.fromJson(json['createdBy']), createdBy: json['createdBy'] != null ? User.fromJson(json['createdBy']) : null,
tags: (json['tags'] as List<dynamic>?) tags: (json['tags'] as List<dynamic>?)
?.map((e) => Tag.fromJson(e)) ?.map((e) => Tag.fromJson(e))
.toList() ?? .toList() ??
@ -99,120 +103,128 @@ class JobData {
} }
class Project { class Project {
final String id; final String? id;
final String name; final String? name;
final String shortName; final String? shortName;
final String assignedDate; final String? assignedDate;
final String contactName; final String? contactName;
final String contactPhone; final String? contactPhone;
final String contactEmail; final String? contactEmail;
Project({ Project({
required this.id, this.id,
required this.name, this.name,
required this.shortName, this.shortName,
required this.assignedDate, this.assignedDate,
required this.contactName, this.contactName,
required this.contactPhone, this.contactPhone,
required this.contactEmail, this.contactEmail,
}); });
factory Project.fromJson(Map<String, dynamic> json) { factory Project.fromJson(Map<String, dynamic>? json) {
if (json == null) return Project();
return Project( return Project(
id: json['id'] as String, id: json['id'] as String?,
name: json['name'] as String, name: json['name'] as String?,
shortName: json['shortName'] as String, shortName: json['shortName'] as String?,
assignedDate: json['assignedDate'] as String, assignedDate: json['assignedDate'] as String?,
contactName: json['contactName'] as String, contactName: json['contactName'] as String?,
contactPhone: json['contactPhone'] as String, contactPhone: json['contactPhone'] as String?,
contactEmail: json['contactEmail'] as String, contactEmail: json['contactEmail'] as String?,
); );
} }
} }
class Assignee { class Assignee {
final String id; final String? id;
final String firstName; final String? firstName;
final String lastName; final String? lastName;
final String? email; final String? email;
final String? photo; final String? photo;
final String jobRoleId; final String? jobRoleId;
final String jobRoleName; final String? jobRoleName;
Assignee({ Assignee({
required this.id, this.id,
required this.firstName, this.firstName,
required this.lastName, this.lastName,
this.email, this.email,
this.photo, this.photo,
required this.jobRoleId, this.jobRoleId,
required this.jobRoleName, this.jobRoleName,
}); });
factory Assignee.fromJson(Map<String, dynamic> json) { factory Assignee.fromJson(Map<String, dynamic>? json) {
if (json == null) return Assignee();
return Assignee( return Assignee(
id: json['id'] as String, id: json['id'] as String?,
firstName: json['firstName'] as String, firstName: json['firstName'] as String?,
lastName: json['lastName'] as String, lastName: json['lastName'] as String?,
email: json['email'] as String?, email: json['email'] as String?,
photo: json['photo'] as String?, photo: json['photo'] as String?,
jobRoleId: json['jobRoleId'] as String, jobRoleId: json['jobRoleId'] as String?,
jobRoleName: json['jobRoleName'] as String, jobRoleName: json['jobRoleName'] as String?,
); );
} }
} }
class Status { class Status {
final String id; final String? id;
final String name; final String? name;
final String displayName; final String? displayName;
final int level; final int? level;
Status({ Status({
required this.id, this.id,
required this.name, this.name,
required this.displayName, this.displayName,
required this.level, this.level,
}); });
factory Status.fromJson(Map<String, dynamic> json) { factory Status.fromJson(Map<String, dynamic>? json) {
if (json == null) return Status();
return Status( return Status(
id: json['id'] as String, id: json['id'] as String?,
name: json['name'] as String, name: json['name'] as String?,
displayName: json['displayName'] as String, displayName: json['displayName'] as String?,
level: json['level'] as int, level: json['level'] as int?,
); );
} }
} }
class User { class User {
final String id; final String? id;
final String firstName; final String? firstName;
final String lastName; final String? lastName;
final String? email; final String? email;
final String? photo; final String? photo;
final String jobRoleId; final String? jobRoleId;
final String jobRoleName; final String? jobRoleName;
User({ User({
required this.id, this.id,
required this.firstName, this.firstName,
required this.lastName, this.lastName,
this.email, this.email,
this.photo, this.photo,
required this.jobRoleId, this.jobRoleId,
required this.jobRoleName, this.jobRoleName,
}); });
factory User.fromJson(Map<String, dynamic> json) { factory User.fromJson(Map<String, dynamic>? json) {
if (json == null) return User();
return User( return User(
id: json['id'] as String, id: json['id'] as String?,
firstName: json['firstName'] as String, firstName: json['firstName'] as String?,
lastName: json['lastName'] as String, lastName: json['lastName'] as String?,
email: json['email'] as String?, email: json['email'] as String?,
photo: json['photo'] as String?, photo: json['photo'] as String?,
jobRoleId: json['jobRoleId'] as String, jobRoleId: json['jobRoleId'] as String?,
jobRoleName: json['jobRoleName'] as String, jobRoleName: json['jobRoleName'] as String?,
); );
} }
} }
@ -223,7 +235,9 @@ class Tag {
Tag({this.id, this.name}); 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( return Tag(
id: json['id'] as String?, id: json['id'] as String?,
name: json['name'] as String?, name: json['name'] as String?,
@ -232,27 +246,29 @@ class Tag {
} }
class UpdateLog { class UpdateLog {
final String id; final String? id;
final Status? status; final Status? status;
final Status nextStatus; final Status? nextStatus;
final String comment; final String? comment;
final User updatedBy; final User? updatedBy;
UpdateLog({ UpdateLog({
required this.id, this.id,
this.status, this.status,
required this.nextStatus, this.nextStatus,
required this.comment, this.comment,
required this.updatedBy, this.updatedBy,
}); });
factory UpdateLog.fromJson(Map<String, dynamic> json) { factory UpdateLog.fromJson(Map<String, dynamic>? json) {
if (json == null) return UpdateLog();
return UpdateLog( return UpdateLog(
id: json['id'] as String, id: json['id'] as String?,
status: json['status'] != null ? Status.fromJson(json['status']) : null, status: json['status'] != null ? Status.fromJson(json['status']) : null,
nextStatus: Status.fromJson(json['nextStatus']), nextStatus: json['nextStatus'] != null ? Status.fromJson(json['nextStatus']) : null,
comment: json['comment'] as String, comment: json['comment'] as String?,
updatedBy: User.fromJson(json['updatedBy']), 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((_) { controller.fetchJobDetail(widget.jobId).then((_) {
final job = controller.jobDetail.value?.data; final job = controller.jobDetail.value?.data;
if (job != null) { if (job != null) {
_titleController.text = job.title; _titleController.text = job.title ?? '';
_descriptionController.text = job.description; _descriptionController.text = job.description ?? '';
_startDateController.text = DateTimeUtils.convertUtcToLocal( _startDateController.text = DateTimeUtils.convertUtcToLocal(
job.startDate, job.startDate ?? DateTime.now().toIso8601String(),
format: "yyyy-MM-dd"); format: "yyyy-MM-dd");
_dueDateController.text = _dueDateController.text = DateTimeUtils.convertUtcToLocal(
DateTimeUtils.convertUtcToLocal(job.dueDate, format: "yyyy-MM-dd"); job.dueDate ?? '',
_selectedAssignees.value = job.assignees; format: "yyyy-MM-dd");
_selectedTags.value = job.tags; _selectedAssignees.value = job.assignees ?? [];
_selectedTags.value = job.tags ?? [];
} }
}); });
} }
@ -114,14 +115,14 @@ class _JobDetailsScreenState extends State<JobDetailsScreen> with UIMixin {
} }
final originalAssignees = job.assignees; final originalAssignees = job.assignees;
final assigneesPayload = originalAssignees.map((a) { final assigneesPayload = originalAssignees?.map((a) {
final isSelected = _selectedAssignees.any((s) => s.id == a.id); final isSelected = _selectedAssignees.any((s) => s.id == a.id);
return {"employeeId": a.id, "isActive": isSelected}; return {"employeeId": a.id, "isActive": isSelected};
}).toList(); }).toList();
for (var s in _selectedAssignees) { for (var s in _selectedAssignees) {
if (!originalAssignees.any((a) => a.id == s.id)) { if (!(originalAssignees?.any((a) => a.id == s.id) ?? false)) {
assigneesPayload.add({"employeeId": s.id, "isActive": true}); assigneesPayload?.add({"employeeId": s.id, "isActive": true});
} }
} }
@ -129,7 +130,7 @@ class _JobDetailsScreenState extends State<JobDetailsScreen> with UIMixin {
{"op": "replace", "path": "/assignees", "value": assigneesPayload}); {"op": "replace", "path": "/assignees", "value": assigneesPayload});
final originalTags = job.tags; final originalTags = job.tags;
final replaceTagsPayload = originalTags.map((t) { final replaceTagsPayload = originalTags?.map((t) {
final isSelected = _selectedTags.any((s) => s.id == t.id); final isSelected = _selectedTags.any((s) => s.id == t.id);
return {"id": t.id, "name": t.name, "isActive": isSelected}; return {"id": t.id, "name": t.name, "isActive": isSelected};
}).toList(); }).toList();
@ -139,7 +140,7 @@ class _JobDetailsScreenState extends State<JobDetailsScreen> with UIMixin {
.map((t) => {"name": t.name, "isActive": true}) .map((t) => {"name": t.name, "isActive": true})
.toList(); .toList();
if (replaceTagsPayload.isNotEmpty) { if ((replaceTagsPayload?.isNotEmpty ?? false)) {
operations operations
.add({"op": "replace", "path": "/tags", "value": replaceTagsPayload}); .add({"op": "replace", "path": "/tags", "value": replaceTagsPayload});
} }
@ -157,7 +158,7 @@ class _JobDetailsScreenState extends State<JobDetailsScreen> with UIMixin {
} }
final success = await ApiService.editServiceProjectJobApi( final success = await ApiService.editServiceProjectJobApi(
jobId: job.id, jobId: job.id ?? "",
operations: operations, operations: operations,
); );
@ -207,7 +208,7 @@ class _JobDetailsScreenState extends State<JobDetailsScreen> with UIMixin {
); );
await controller.updateJobAttendance( await controller.updateJobAttendance(
jobId: job.id, jobId: job.id ?? "",
action: action == 0 ? 0 : 1, action: action == 0 ? 0 : 1,
comment: comment, comment: comment,
attachment: attachmentFile, attachment: attachmentFile,
@ -352,14 +353,14 @@ class _JobDetailsScreenState extends State<JobDetailsScreen> with UIMixin {
onTap: () async { onTap: () async {
final initiallySelected = assignees.map<EmployeeModel>((a) { final initiallySelected = assignees.map<EmployeeModel>((a) {
return EmployeeModel( return EmployeeModel(
id: a.id, id: a.id ?? '',
employeeId: a.id, employeeId: a.id ?? '',
firstName: a.firstName, firstName: a.firstName ?? '',
lastName: a.lastName, lastName: a.lastName ?? '',
name: "${a.firstName} ${a.lastName}", name: "${a.firstName} ${a.lastName}",
designation: a.jobRoleName, designation: a.jobRoleName ?? '',
jobRole: a.jobRoleName, jobRole: a.jobRoleName ?? '',
jobRoleID: a.jobRoleId, jobRoleID: a.jobRoleId ?? '',
email: a.email ?? '', email: a.email ?? '',
phoneNumber: '', phoneNumber: '',
activity: 0, activity: 0,
@ -495,7 +496,7 @@ class _JobDetailsScreenState extends State<JobDetailsScreen> with UIMixin {
onPressed: () async { onPressed: () async {
isAttendanceExpanded.value = isAttendanceExpanded.value =
!isAttendanceExpanded.value; !isAttendanceExpanded.value;
if (isAttendanceExpanded.value && job != null) { if (isAttendanceExpanded.value ) {
await controller await controller
.fetchJobAttendanceLog(job.attendanceId ?? ''); .fetchJobAttendanceLog(job.attendanceId ?? '');
} }
@ -774,11 +775,11 @@ class _JobDetailsScreenState extends State<JobDetailsScreen> with UIMixin {
titleIcon: Icons.label_outline, titleIcon: Icons.label_outline,
children: [_tagEditor()]), children: [_tagEditor()]),
MySpacing.height(16), MySpacing.height(16),
if (job.updateLogs.isNotEmpty) if ((job.updateLogs?.isNotEmpty ?? false))
_buildSectionCard( _buildSectionCard(
title: "Update Logs", title: "Update Logs",
titleIcon: Icons.history, titleIcon: Icons.history,
children: [JobTimeline(logs: job.updateLogs)]), children: [JobTimeline(logs: job.updateLogs ?? [])]),
MySpacing.height(80), MySpacing.height(80),
], ],
), ),
@ -806,12 +807,14 @@ class JobTimeline extends StatelessWidget {
itemBuilder: (_, index) { itemBuilder: (_, index) {
final log = reversedLogs[index]; final log = reversedLogs[index];
final statusName = log.status?.displayName ?? "Created"; final statusName = log.status?.displayName ?? "Created";
final nextStatusName = log.nextStatus.displayName; final nextStatusName = log.nextStatus?.displayName ?? "N/A";
final comment = log.comment; final comment = log.comment ?? '';
final updatedBy = 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 = 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( return TimelineTile(
alignment: TimelineAlign.start, alignment: TimelineAlign.start,