From 8edd1894795bd1741d55734fabb5dbf07012802b Mon Sep 17 00:00:00 2001 From: Vaibhav Surve Date: Wed, 19 Nov 2025 16:36:32 +0530 Subject: [PATCH] feat: add branch selection functionality and API integration for service project jobs --- .../add_service_project_job_controller.dart | 56 ++++--- lib/helpers/services/api_endpoints.dart | 1 + lib/helpers/services/api_service.dart | 48 ++++++ .../add_service_project_job_bottom_sheet.dart | 52 ++++++ .../service_project_branches_model.dart | 149 ++++++++++++++++++ 5 files changed, 281 insertions(+), 25 deletions(-) create mode 100644 lib/model/service_project/service_project_branches_model.dart diff --git a/lib/controller/service_project/add_service_project_job_controller.dart b/lib/controller/service_project/add_service_project_job_controller.dart index e53fcd6..cf29f3f 100644 --- a/lib/controller/service_project/add_service_project_job_controller.dart +++ b/lib/controller/service_project/add_service_project_job_controller.dart @@ -1,56 +1,58 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:marco/helpers/services/api_service.dart'; -import 'package:marco/model/employees/employee_model.dart'; import 'package:marco/helpers/widgets/my_snackbar.dart'; import 'package:marco/controller/service_project/service_project_details_screen_controller.dart'; +import 'package:marco/model/employees/employee_model.dart'; +import 'package:marco/model/service_project/service_project_branches_model.dart'; class AddServiceProjectJobController extends GetxController { -// Form Controllers + // FORM CONTROLLERS final titleCtrl = TextEditingController(); final descCtrl = TextEditingController(); final tagCtrl = TextEditingController(); - final FocusNode searchFocusNode = FocusNode(); - final RxBool showEmployeePicker = true.obs; + final searchFocusNode = FocusNode(); -// Observables + // OBSERVABLES final startDate = Rx(DateTime.now()); final dueDate = Rx(DateTime.now().add(const Duration(days: 1))); + final enteredTags = [].obs; - - final employees = [].obs; final selectedAssignees = [].obs; - final isSearchingEmployees = false.obs; -// Loading states + // Branches + final branches = [].obs; + final selectedBranch = Rxn(); + final isBranchLoading = false.obs; + + // Loading final isLoading = false.obs; - final isAllEmployeeLoading = false.obs; - final allEmployees = [].obs; - final employeeSearchResults = [].obs; - - @override - void onInit() { - super.onInit(); - } @override void onClose() { titleCtrl.dispose(); descCtrl.dispose(); tagCtrl.dispose(); + searchFocusNode.dispose(); super.onClose(); } - /// Toggle employee selection - void toggleAssignee(EmployeeModel employee) { - if (selectedAssignees.contains(employee)) { - selectedAssignees.remove(employee); - } else { - selectedAssignees.add(employee); + // FETCH BRANCHES + Future fetchBranches(String projectId) async { + isBranchLoading.value = true; + + final response = await ApiService.getServiceProjectBranchesFull( + projectId: projectId, + ); + + if (response != null && response.success) { + branches.assignAll(response.data?.data ?? []); } + + isBranchLoading.value = false; } - /// Create Service Project Job API call + // CREATE JOB Future createJob(String projectId) async { if (titleCtrl.text.trim().isEmpty || descCtrl.text.trim().isEmpty) { showAppSnackbar( @@ -63,18 +65,22 @@ class AddServiceProjectJobController extends GetxController { final assigneeIds = selectedAssignees.map((e) => e.id).toList(); + isLoading.value = true; + final success = await ApiService.createServiceProjectJobApi( title: titleCtrl.text.trim(), description: descCtrl.text.trim(), projectId: projectId, + branchId: selectedBranch.value?.id, assignees: assigneeIds.map((id) => {"id": id}).toList(), startDate: startDate.value!, dueDate: dueDate.value!, tags: enteredTags.map((tag) => {"name": tag}).toList(), ); + isLoading.value = false; + if (success) { - // 🔥 Auto-refresh job list in ServiceProjectDetailsController if (Get.isRegistered()) { Get.find().refreshJobsAfterAdd(); } diff --git a/lib/helpers/services/api_endpoints.dart b/lib/helpers/services/api_endpoints.dart index 33c0ed6..f68ad6f 100644 --- a/lib/helpers/services/api_endpoints.dart +++ b/lib/helpers/services/api_endpoints.dart @@ -151,4 +151,5 @@ class ApiEndpoints { static const String getServiceProjectUpateJobAllocationList = "/serviceproject/get/allocation/list"; static const String manageServiceProjectUpateJobAllocation = "/serviceproject/manage/allocation"; static const String getTeamRoles = "/master/team-roles/list"; + static const String getServiceProjectBranches = "/serviceproject/branch/list"; } diff --git a/lib/helpers/services/api_service.dart b/lib/helpers/services/api_service.dart index f3d7ce2..65a8fa3 100644 --- a/lib/helpers/services/api_service.dart +++ b/lib/helpers/services/api_service.dart @@ -39,6 +39,7 @@ import 'package:marco/model/service_project/job_list_model.dart'; import 'package:marco/model/service_project/service_project_job_detail_model.dart'; import 'package:marco/model/service_project/job_attendance_logs_model.dart'; import 'package:marco/model/service_project/job_allocation_model.dart'; +import 'package:marco/model/service_project/service_project_branches_model.dart'; class ApiService { static const bool enableLogs = true; @@ -310,6 +311,51 @@ class ApiService { } } + /// Fetch Service Project Branches with full response + static Future getServiceProjectBranchesFull({ + required String projectId, + int pageNumber = 1, + int pageSize = 20, + String searchString = '', + bool isActive = true, + }) async { + final queryParams = { + 'pageNumber': pageNumber.toString(), + 'pageSize': pageSize.toString(), + 'searchString': searchString, + 'isActive': isActive.toString(), + }; + + final endpoint = "${ApiEndpoints.getServiceProjectBranches}/$projectId"; + + try { + final response = await _getRequest( + endpoint, + queryParams: queryParams, + ); + + if (response == null) { + _log("getServiceProjectBranchesFull: No response received."); + return null; + } + + final parsedJson = _parseResponseForAllData( + response, + label: "ServiceProjectBranchesFull", + ); + + if (parsedJson == null) return null; + + return ServiceProjectBranchesResponse.fromJson(parsedJson); + } catch (e, stack) { + _log( + "Exception in getServiceProjectBranchesFull: $e\n$stack", + level: LogLevel.error, + ); + return null; + } + } + // Service Project Module APIs static Future?> getTeamRoles() async { try { @@ -558,6 +604,7 @@ class ApiService { required DateTime startDate, required DateTime dueDate, required List> tags, + required String? branchId, }) async { const endpoint = ApiEndpoints.createServiceProjectJob; logSafe("Creating Service Project Job for projectId: $projectId"); @@ -570,6 +617,7 @@ class ApiService { "startDate": startDate.toIso8601String(), "dueDate": dueDate.toIso8601String(), "tags": tags, + "branchId": branchId, }; try { diff --git a/lib/model/service_project/add_service_project_job_bottom_sheet.dart b/lib/model/service_project/add_service_project_job_bottom_sheet.dart index 081c0b3..a08e0be 100644 --- a/lib/model/service_project/add_service_project_job_bottom_sheet.dart +++ b/lib/model/service_project/add_service_project_job_bottom_sheet.dart @@ -9,6 +9,7 @@ import 'package:marco/helpers/widgets/date_range_picker.dart'; import 'package:marco/controller/service_project/add_service_project_job_controller.dart'; import 'package:marco/model/employees/employee_model.dart'; import 'package:marco/model/employees/multiple_select_bottomsheet.dart'; +import 'package:marco/model/service_project/service_project_branches_model.dart'; class AddServiceProjectJobBottomSheet extends StatefulWidget { final String projectId; @@ -31,6 +32,7 @@ class _AddServiceProjectJobBottomSheetState super.initState(); _selectedEmployees = RxList.from(controller.selectedAssignees); + controller.fetchBranches(widget.projectId); } @override @@ -89,6 +91,54 @@ class _AddServiceProjectJobBottomSheetState ), ], ); + Widget _branchSelector() => Obx(() { + if (controller.isBranchLoading.value) { + return const Center(child: CircularProgressIndicator()); + } + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + MyText.labelMedium("Select Branch (Optional)"), + MySpacing.height(8), + PopupMenuButton( + onSelected: (branch) { + controller.selectedBranch.value = branch; + }, + itemBuilder: (_) => controller.branches + .map( + (b) => PopupMenuItem( + value: b, + child: Text(b.branchName), + ), + ) + .toList(), + child: Container( + padding: + const EdgeInsets.symmetric(horizontal: 14, vertical: 14), + decoration: BoxDecoration( + color: Colors.grey.shade100, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.grey.shade300), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Obx(() => Text( + controller.selectedBranch.value?.branchName ?? + "Select Branch (Optional)", + style: MyTextStyle.bodySmall( + color: Colors.grey.shade700, + ), + )), + const Icon(Icons.arrow_drop_down), + ], + ), + ), + ), + ], + ); + }); Widget _employeeSelector() => Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -203,6 +253,8 @@ class _AddServiceProjectJobBottomSheetState MySpacing.height(16), _employeeSelector(), MySpacing.height(16), + _branchSelector(), + MySpacing.height(16), _labelWithStar("Tags", required: true), MySpacing.height(8), _tagInput(), diff --git a/lib/model/service_project/service_project_branches_model.dart b/lib/model/service_project/service_project_branches_model.dart new file mode 100644 index 0000000..6bdc639 --- /dev/null +++ b/lib/model/service_project/service_project_branches_model.dart @@ -0,0 +1,149 @@ +class ServiceProjectBranchesResponse { + final bool success; + final String message; + final Data? data; + final dynamic errors; + final int statusCode; + final DateTime timestamp; + + ServiceProjectBranchesResponse({ + required this.success, + required this.message, + required this.data, + required this.errors, + required this.statusCode, + required this.timestamp, + }); + + factory ServiceProjectBranchesResponse.fromJson(Map json) { + return ServiceProjectBranchesResponse( + success: json['success'] ?? false, + message: json['message'] ?? '', + data: json['data'] != null ? Data.fromJson(json['data']) : null, + errors: json['errors'], + statusCode: json['statusCode'] ?? 0, + timestamp: DateTime.tryParse(json['timestamp'] ?? '') ?? DateTime.now(), + ); + } +} + +class Data { + final int currentPage; + final int totalPages; + final int totalEntities; + final List data; + + Data({ + required this.currentPage, + required this.totalPages, + required this.totalEntities, + required this.data, + }); + + factory Data.fromJson(Map json) { + return Data( + currentPage: json['currentPage'] ?? 0, + totalPages: json['totalPages'] ?? 0, + totalEntities: json['totalEntities'] ?? 0, + data: json['data'] != null + ? List.from( + json['data'].map((x) => Branch.fromJson(x)), + ) + : [], + ); + } +} + +class Branch { + final String id; + final String branchName; + final Project project; + final String? contactInformation; + final String? address; + final String? branchType; + final DateTime? createdAt; + final CreatedBy? createdBy; + + Branch({ + required this.id, + required this.branchName, + required this.project, + this.contactInformation, + this.address, + this.branchType, + this.createdAt, + this.createdBy, + }); + + factory Branch.fromJson(Map json) { + return Branch( + id: json['id'] ?? '', + branchName: json['branchName'] ?? '', + project: Project.fromJson(json['project'] ?? {}), + contactInformation: json['contactInformation'], + address: json['address'], + branchType: json['branchType'], + createdAt: + json['createdAt'] != null ? DateTime.parse(json['createdAt']) : null, + createdBy: + json['createdBy'] != null ? CreatedBy.fromJson(json['createdBy']) : null, + ); + } +} + +class Project { + final String id; + final String name; + final String shortName; + final DateTime? assignedDate; + + Project({ + required this.id, + required this.name, + required this.shortName, + this.assignedDate, + }); + + factory Project.fromJson(Map json) { + return Project( + id: json['id'] ?? '', + name: json['name'] ?? '', + shortName: json['shortName'] ?? '', + assignedDate: json['assignedDate'] != null + ? DateTime.parse(json['assignedDate']) + : null, + ); + } +} + +class CreatedBy { + final String id; + final String firstName; + final String lastName; + final String email; + final String? photo; + final String jobRoleId; + final String jobRoleName; + + CreatedBy({ + required this.id, + required this.firstName, + required this.lastName, + required this.email, + this.photo, + required this.jobRoleId, + required this.jobRoleName, + }); + + factory CreatedBy.fromJson(Map json) { + return CreatedBy( + id: json['id'] ?? '', + firstName: json['firstName'] ?? '', + lastName: json['lastName'] ?? '', + email: json['email'] ?? '', + photo: json['photo'], + jobRoleId: json['jobRoleId'] ?? '', + jobRoleName: json['jobRoleName'] ?? '', + ); + } +}