From 605617695e80c2fbc5fbfac2f9f0177dbb24f260 Mon Sep 17 00:00:00 2001 From: Vaibhav Surve Date: Mon, 17 Nov 2025 16:02:03 +0530 Subject: [PATCH] refactor: improve code readability and consistency in JobDetailsScreen --- .../service_project_job_detail_screen.dart | 585 +++++++++--------- 1 file changed, 276 insertions(+), 309 deletions(-) diff --git a/lib/view/service_project/service_project_job_detail_screen.dart b/lib/view/service_project/service_project_job_detail_screen.dart index 7f06261..2ed6ee4 100644 --- a/lib/view/service_project/service_project_job_detail_screen.dart +++ b/lib/view/service_project/service_project_job_detail_screen.dart @@ -24,16 +24,15 @@ class JobDetailsScreen extends StatefulWidget { class _JobDetailsScreenState extends State with UIMixin { late final ServiceProjectDetailsController controller; - final _titleController = TextEditingController(); - final _descriptionController = TextEditingController(); - final _startDateController = TextEditingController(); - final _dueDateController = TextEditingController(); + final TextEditingController _titleController = TextEditingController(); + final TextEditingController _descriptionController = TextEditingController(); + final TextEditingController _startDateController = TextEditingController(); + final TextEditingController _dueDateController = TextEditingController(); + final TextEditingController _tagTextController = TextEditingController(); - RxList _selectedAssignees = [].obs; - RxList _selectedTags = [].obs; - final _tagTextController = TextEditingController(); - - RxBool isEditing = false.obs; // track edit/view mode + final RxList _selectedAssignees = [].obs; + final RxList _selectedTags = [].obs; + final RxBool isEditing = false.obs; @override void initState() { @@ -49,8 +48,8 @@ class _JobDetailsScreenState extends State with UIMixin { format: "yyyy-MM-dd"); _dueDateController.text = DateTimeUtils.convertUtcToLocal(job.dueDate, format: "yyyy-MM-dd"); - _selectedAssignees.assignAll(job.assignees); - _selectedTags.assignAll(job.tags); + _selectedAssignees.value = job.assignees; + _selectedTags.value = job.tags; } }); } @@ -69,27 +68,23 @@ class _JobDetailsScreenState extends State with UIMixin { final job = controller.jobDetail.value?.data; if (job == null) return; - final operations = >[]; + final List> operations = []; - // Title - if (_titleController.text.trim() != job.title) { - operations.add({ - "op": "replace", - "path": "/title", - "value": _titleController.text.trim(), - }); + final trimmedTitle = _titleController.text.trim(); + if (trimmedTitle != job.title) { + operations + .add({"op": "replace", "path": "/title", "value": trimmedTitle}); } - // Description - if (_descriptionController.text.trim() != job.description) { + final trimmedDescription = _descriptionController.text.trim(); + if (trimmedDescription != job.description) { operations.add({ "op": "replace", "path": "/description", - "value": _descriptionController.text.trim(), + "value": trimmedDescription }); } - // Dates final startDate = DateTime.tryParse(_startDateController.text); final dueDate = DateTime.tryParse(_dueDateController.text); @@ -97,7 +92,7 @@ class _JobDetailsScreenState extends State with UIMixin { operations.add({ "op": "replace", "path": "/startDate", - "value": startDate.toUtc().toIso8601String(), + "value": startDate.toUtc().toIso8601String() }); } @@ -105,69 +100,44 @@ class _JobDetailsScreenState extends State with UIMixin { operations.add({ "op": "replace", "path": "/dueDate", - "value": dueDate.toUtc().toIso8601String(), + "value": dueDate.toUtc().toIso8601String() }); } - // Assignees - final originalAssignees = job.assignees ?? []; + final originalAssignees = job.assignees; final assigneesPayload = originalAssignees.map((a) { final isSelected = _selectedAssignees.any((s) => s.id == a.id); - return { - "employeeId": a.id, - "isActive": isSelected, - }; + return {"employeeId": a.id, "isActive": isSelected}; }).toList(); - _selectedAssignees.forEach((s) { + for (var s in _selectedAssignees) { if (!originalAssignees.any((a) => a.id == s.id)) { - assigneesPayload.add({ - "employeeId": s.id, - "isActive": true, - }); + assigneesPayload.add({"employeeId": s.id, "isActive": true}); } - }); + } - operations.add({ - "op": "replace", - "path": "/assignees", - "value": assigneesPayload, - }); + operations.add( + {"op": "replace", "path": "/assignees", "value": assigneesPayload}); - // Tags - final originalTags = job.tags ?? []; + final originalTags = job.tags; final replaceTagsPayload = originalTags.map((t) { 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(); final addTagsPayload = _selectedTags .where((t) => t.id == "0") - .map((t) => { - "name": t.name, - "isActive": true, - }) + .map((t) => {"name": t.name, "isActive": true}) .toList(); if (replaceTagsPayload.isNotEmpty) { - operations.add({ - "op": "replace", - "path": "/tags", - "value": replaceTagsPayload, - }); + operations + .add({"op": "replace", "path": "/tags", "value": replaceTagsPayload}); } if (addTagsPayload.isNotEmpty) { - operations.add({ - "op": "add", - "path": "/tags", - "value": addTagsPayload, - }); + operations.add({"op": "add", "path": "/tags", "value": addTagsPayload}); } if (operations.isEmpty) { @@ -181,7 +151,7 @@ class _JobDetailsScreenState extends State with UIMixin { if (success) { Get.snackbar("Success", "Job updated successfully"); await controller.fetchJobDetail(widget.jobId); - isEditing.value = false; // switch back to view mode + isEditing.value = false; } else { Get.snackbar("Error", "Failed to update job. Check inputs or try again."); } @@ -199,225 +169,227 @@ class _JobDetailsScreenState extends State with UIMixin { margin: const EdgeInsets.symmetric(vertical: 8), child: Padding( padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Icon(titleIcon, size: 20, color: Colors.blueAccent), - MySpacing.width(8), - MyText.bodyLarge(title, fontWeight: 700, fontSize: 16), - ], - ), - MySpacing.height(8), - const Divider(), - ...children, - ], - ), + child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Row(children: [ + Icon(titleIcon, size: 20, color: Colors.blueAccent), + MySpacing.width(8), + MyText.bodyLarge(title, fontWeight: 700, fontSize: 16), + ]), + MySpacing.height(8), + const Divider(), + ...children, + ]), ), ); } Widget _editableRow(String label, TextEditingController controller) { - return Obx(() => Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - MyText.bodySmall(label, - color: Colors.grey[600], fontWeight: 600), - const SizedBox(height: 6), - isEditing.value - ? TextField( - controller: controller, - decoration: InputDecoration( - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(5)), - isDense: true, - contentPadding: - const EdgeInsets.symmetric(horizontal: 10, vertical: 12), - ), - ) - : Container( - width: double.infinity, - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 14), - decoration: BoxDecoration( - color: Colors.grey.shade100, - borderRadius: BorderRadius.circular(5), - ), - child: Text(controller.text, style: const TextStyle(fontSize: 14)), - ), - ], - ), - )); + return Obx(() { + final editing = isEditing.value; + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + MyText.bodySmall(label, color: Colors.grey[600], fontWeight: 600), + const SizedBox(height: 6), + editing + ? TextField( + controller: controller, + decoration: InputDecoration( + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(5)), + isDense: true, + contentPadding: const EdgeInsets.symmetric( + horizontal: 10, vertical: 12), + ), + ) + : Container( + width: double.infinity, + padding: + const EdgeInsets.symmetric(horizontal: 12, vertical: 14), + decoration: BoxDecoration( + color: Colors.grey.shade100, + borderRadius: BorderRadius.circular(5)), + child: Text(controller.text, + style: const TextStyle(fontSize: 14)), + ), + ]), + ); + }); } - Widget _assigneeSelector() { - return Obx(() => Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - MyText.bodySmall("Assigned To", - color: Colors.grey[600], fontWeight: 600), - const SizedBox(height: 8), - GestureDetector( - onTap: isEditing.value - ? () async { - final initiallySelected = - _selectedAssignees.map((a) { - return EmployeeModel( - 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 ?? '', - email: a.email ?? '', - phoneNumber: '', - activity: 0, - action: 0, - ); - }).toList(); - - final selected = - await showModalBottomSheet>( - context: context, - isScrollControlled: true, - builder: (_) => EmployeeSelectionBottomSheet( - multipleSelection: true, - initiallySelected: initiallySelected, - ), - ); - - if (selected != null) { - final newAssignees = selected.map((e) { - return Assignee( - id: e.id, - firstName: e.firstName, - lastName: e.lastName, - email: e.email ?? '', - photo: '', - jobRoleId: e.jobRoleID ?? '', - jobRoleName: e.jobRole ?? '', - ); - }).toList(); - - _selectedAssignees.assignAll(newAssignees); - } - } - : null, - child: Container( + Widget _dateRangePicker() { + return Obx(() { + final editing = isEditing.value; + final startDate = + DateTime.tryParse(_startDateController.text) ?? DateTime.now(); + final dueDate = DateTime.tryParse(_dueDateController.text) ?? + DateTime.now().add(const Duration(days: 1)); + return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + MyText.bodySmall("Select Date Range", + color: Colors.grey[600], fontWeight: 600), + const SizedBox(height: 8), + editing + ? DateRangePickerWidget( + startDate: Rx(startDate), + endDate: Rx(dueDate), + startLabel: "Start Date", + endLabel: "Due Date", + onDateRangeSelected: (start, end) { + if (start != null && end != null) { + _startDateController.text = + start.toIso8601String().split("T").first; + _dueDateController.text = + end.toIso8601String().split("T").first; + } + }, + ) + : Container( width: double.infinity, padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 14), decoration: BoxDecoration( - color: Colors.grey.shade100, - border: Border.all(color: Colors.grey.shade400), - borderRadius: BorderRadius.circular(5), - ), + color: Colors.grey.shade100, + borderRadius: BorderRadius.circular(5)), child: Text( - _selectedAssignees.isEmpty - ? "No Assignees" - : _selectedAssignees - .map((e) => "${e.firstName} ${e.lastName}") - .join(", "), - style: const TextStyle(fontSize: 14), + "${_startDateController.text} → ${_dueDateController.text}"), + ), + ]); + }); + } + + Widget _assigneeInputWithChips() { + return Obx(() { + final editing = isEditing.value; + final assignees = _selectedAssignees; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (assignees.isNotEmpty) + Wrap( + spacing: 6, + children: assignees + .map( + (a) => Chip( + label: Text("${a.firstName} ${a.lastName}"), + onDeleted: editing + ? () { + _selectedAssignees.remove(a); + } + : null, + ), + ) + .toList(), + ), + const SizedBox(height: 8), + if (editing) + GestureDetector( + onTap: () async { + final initiallySelected = assignees.map((a) { + return EmployeeModel( + 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, + email: a.email, + phoneNumber: '', + activity: 0, + action: 0, + ); + }).toList(); + + final selected = + await showModalBottomSheet>( + context: context, + isScrollControlled: true, + builder: (_) => EmployeeSelectionBottomSheet( + multipleSelection: true, + initiallySelected: initiallySelected, + ), + ); + + if (selected != null) { + _selectedAssignees.value = selected.map((e) { + return Assignee( + id: e.id, + firstName: e.firstName, + lastName: e.lastName, + email: e.email, + photo: '', + jobRoleId: e.jobRoleID, + jobRoleName: e.jobRole, + ); + }).toList(); + } + }, + child: Container( + height: 40, + padding: const EdgeInsets.symmetric(horizontal: 12), + decoration: BoxDecoration( + color: Colors.grey.shade100, + borderRadius: BorderRadius.circular(5), + border: Border.all(color: Colors.grey.shade400), + ), + alignment: Alignment.centerLeft, + child: Text( + "Tap to select assignees", + style: TextStyle(fontSize: 14, color: Colors.grey[700]), ), ), ), - ], - )); - } - - Widget _dateRangePicker() { - return Obx(() => Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - MyText.bodySmall("Select Date Range", - color: Colors.grey[600], fontWeight: 600), - const SizedBox(height: 8), - isEditing.value - ? DateRangePickerWidget( - startDate: Rx( - DateTime.tryParse(_startDateController.text) ?? - DateTime.now()), - endDate: Rx( - DateTime.tryParse(_dueDateController.text) ?? - DateTime.now().add(const Duration(days: 1))), - startLabel: "Start Date", - endLabel: "Due Date", - onDateRangeSelected: (start, end) { - if (start != null && end != null) { - _startDateController.text = - start.toIso8601String().split("T").first; - _dueDateController.text = - end.toIso8601String().split("T").first; - } - }, - ) - : Container( - width: double.infinity, - padding: const EdgeInsets.symmetric( - horizontal: 12, vertical: 14), - decoration: BoxDecoration( - color: Colors.grey.shade100, - borderRadius: BorderRadius.circular(5), - ), - child: Text( - "${_startDateController.text} → ${_dueDateController.text}"), - ), - ], - )); + ], + ); + }); } Widget _tagEditor() { - return Obx(() => Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - MyText.bodySmall("Tags", - color: Colors.grey[600], fontWeight: 600), - const SizedBox(height: 8), - if (isEditing.value) - TextField( - controller: _tagTextController, - onSubmitted: (v) { - final value = v.trim(); - if (value.isNotEmpty && - !_selectedTags.any((t) => t.name == value)) { - setState(() { - _selectedTags.add(Tag(id: "0", name: value)); - }); - } - _tagTextController.clear(); - }, - decoration: InputDecoration( - hintText: "Type and press enter to add tags", - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(5)), - isDense: true, - contentPadding: - const EdgeInsets.symmetric(horizontal: 10, vertical: 12), - ), + return Obx(() { + final editing = isEditing.value; + final tags = _selectedTags; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Wrap( + spacing: 6, + children: tags + .map( + (t) => Chip( + label: Text(t.name), + onDeleted: editing + ? () { + _selectedTags.remove(t); + } + : null, + ), + ) + .toList(), + ), + const SizedBox(height: 8), + if (editing) + TextField( + controller: _tagTextController, + onSubmitted: (v) { + final value = v.trim(); + if (value.isNotEmpty && !tags.any((t) => t.name == value)) { + _selectedTags.add(Tag(id: "0", name: value)); + } + _tagTextController.clear(); + }, + decoration: InputDecoration( + hintText: "Type and press enter to add tags", + border: + OutlineInputBorder(borderRadius: BorderRadius.circular(5)), + isDense: true, + contentPadding: + const EdgeInsets.symmetric(horizontal: 10, vertical: 12), ), - const SizedBox(height: 8), - Wrap( - spacing: 6, - children: _selectedTags - .map((t) => Chip( - label: Text(t.name), - onDeleted: isEditing.value - ? () { - setState(() { - _selectedTags.remove(t); - }); - } - : null, - )) - .toList(), ), - ], - )); + ], + ); + }); } @override @@ -425,11 +397,11 @@ class _JobDetailsScreenState extends State with UIMixin { return Scaffold( backgroundColor: const Color(0xFFF5F5F5), appBar: CustomAppBar( - title: "Service Project Job Details", - onBackPressed: () => Get.back(), - ), + title: "Service Project Job Details", + onBackPressed: () => Get.back()), floatingActionButton: Obx(() => FloatingActionButton.extended( - onPressed: isEditing.value ? _editJob : () => isEditing.value = true, + onPressed: + isEditing.value ? _editJob : () => isEditing.value = true, label: Text(isEditing.value ? "Save" : "Edit"), icon: Icon(isEditing.value ? Icons.save : Icons.edit), )), @@ -463,20 +435,21 @@ class _JobDetailsScreenState extends State with UIMixin { ], ), MySpacing.height(12), - _assigneeSelector(), + _buildSectionCard( + title: "Assignees", + titleIcon: Icons.person_outline, + children: [_assigneeInputWithChips()]), MySpacing.height(16), _buildSectionCard( - title: "Tags", - titleIcon: Icons.label_outline, - children: [_tagEditor()], - ), + title: "Tags", + titleIcon: Icons.label_outline, + children: [_tagEditor()]), MySpacing.height(16), if (job.updateLogs.isNotEmpty) _buildSectionCard( - title: "Update Logs", - titleIcon: Icons.history, - children: [JobTimeline(logs: job.updateLogs)], - ), + title: "Update Logs", + titleIcon: Icons.history, + children: [JobTimeline(logs: job.updateLogs)]), MySpacing.height(80), ], ), @@ -509,51 +482,45 @@ class JobTimeline extends StatelessWidget { final updatedBy = "${log.updatedBy.firstName} ${log.updatedBy.lastName}"; final initials = - "${log.updatedBy.firstName.isNotEmpty ? log.updatedBy.firstName[0] : ''}" - "${log.updatedBy.lastName.isNotEmpty ? log.updatedBy.lastName[0] : ''}"; + "${log.updatedBy.firstName.isNotEmpty ? log.updatedBy.firstName[0] : ''}${log.updatedBy.lastName.isNotEmpty ? log.updatedBy.lastName[0] : ''}"; return TimelineTile( alignment: TimelineAlign.start, isFirst: index == 0, isLast: index == reversedLogs.length - 1, - indicatorStyle: IndicatorStyle( + indicatorStyle: const IndicatorStyle( width: 16, height: 16, - indicator: Container( - decoration: const BoxDecoration( - color: Colors.blue, shape: BoxShape.circle), - ), + indicator: DecoratedBox( + decoration: + BoxDecoration(color: Colors.blue, shape: BoxShape.circle)), ), beforeLineStyle: LineStyle(color: Colors.grey.shade300, thickness: 2), endChild: Padding( padding: const EdgeInsets.all(12), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - MyText.bodyMedium("$statusName → $nextStatusName", - fontWeight: 600), - if (comment.isNotEmpty) ...[ - const SizedBox(height: 8), - MyText.bodyMedium(comment), - ], - const SizedBox(height: 10), - Row( - children: [ - Container( - padding: const EdgeInsets.symmetric( - horizontal: 6, vertical: 2), - decoration: BoxDecoration( - color: Colors.grey.shade300, - borderRadius: BorderRadius.circular(4)), - child: MyText.bodySmall(initials, fontWeight: 600), - ), - const SizedBox(width: 6), - Expanded(child: MyText.bodySmall(updatedBy)), - ], - ), - const SizedBox(height: 10), + child: + Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + MyText.bodyMedium("$statusName → $nextStatusName", + fontWeight: 600), + if (comment.isNotEmpty) ...[ + const SizedBox(height: 8), + MyText.bodyMedium(comment), ], - ), + const SizedBox(height: 10), + Row(children: [ + Container( + padding: + const EdgeInsets.symmetric(horizontal: 6, vertical: 2), + decoration: BoxDecoration( + color: Colors.grey.shade300, + borderRadius: BorderRadius.circular(4)), + child: MyText.bodySmall(initials, fontWeight: 600), + ), + const SizedBox(width: 6), + Expanded(child: MyText.bodySmall(updatedBy)), + ]), + const SizedBox(height: 10), + ]), ), ); },