feat: add branch selection functionality and API integration for service project jobs

This commit is contained in:
Vaibhav Surve 2025-11-19 16:36:32 +05:30
parent bbadcc4139
commit 8edd189479
5 changed files with 281 additions and 25 deletions

View File

@ -1,56 +1,58 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:marco/helpers/services/api_service.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/helpers/widgets/my_snackbar.dart';
import 'package:marco/controller/service_project/service_project_details_screen_controller.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 { class AddServiceProjectJobController extends GetxController {
// Form Controllers // FORM CONTROLLERS
final titleCtrl = TextEditingController(); final titleCtrl = TextEditingController();
final descCtrl = TextEditingController(); final descCtrl = TextEditingController();
final tagCtrl = TextEditingController(); final tagCtrl = TextEditingController();
final FocusNode searchFocusNode = FocusNode(); final searchFocusNode = FocusNode();
final RxBool showEmployeePicker = true.obs;
// Observables // OBSERVABLES
final startDate = Rx<DateTime?>(DateTime.now()); final startDate = Rx<DateTime?>(DateTime.now());
final dueDate = Rx<DateTime?>(DateTime.now().add(const Duration(days: 1))); final dueDate = Rx<DateTime?>(DateTime.now().add(const Duration(days: 1)));
final enteredTags = <String>[].obs; final enteredTags = <String>[].obs;
final employees = <EmployeeModel>[].obs;
final selectedAssignees = <EmployeeModel>[].obs; final selectedAssignees = <EmployeeModel>[].obs;
final isSearchingEmployees = false.obs;
// Loading states // Branches
final branches = <Branch>[].obs;
final selectedBranch = Rxn<Branch>();
final isBranchLoading = false.obs;
// Loading
final isLoading = false.obs; final isLoading = false.obs;
final isAllEmployeeLoading = false.obs;
final allEmployees = <EmployeeModel>[].obs;
final employeeSearchResults = <EmployeeModel>[].obs;
@override
void onInit() {
super.onInit();
}
@override @override
void onClose() { void onClose() {
titleCtrl.dispose(); titleCtrl.dispose();
descCtrl.dispose(); descCtrl.dispose();
tagCtrl.dispose(); tagCtrl.dispose();
searchFocusNode.dispose();
super.onClose(); super.onClose();
} }
/// Toggle employee selection // FETCH BRANCHES
void toggleAssignee(EmployeeModel employee) { Future<void> fetchBranches(String projectId) async {
if (selectedAssignees.contains(employee)) { isBranchLoading.value = true;
selectedAssignees.remove(employee);
} else { final response = await ApiService.getServiceProjectBranchesFull(
selectedAssignees.add(employee); projectId: projectId,
);
if (response != null && response.success) {
branches.assignAll(response.data?.data ?? []);
} }
isBranchLoading.value = false;
} }
/// Create Service Project Job API call // CREATE JOB
Future<void> createJob(String projectId) async { Future<void> createJob(String projectId) async {
if (titleCtrl.text.trim().isEmpty || descCtrl.text.trim().isEmpty) { if (titleCtrl.text.trim().isEmpty || descCtrl.text.trim().isEmpty) {
showAppSnackbar( showAppSnackbar(
@ -63,18 +65,22 @@ class AddServiceProjectJobController extends GetxController {
final assigneeIds = selectedAssignees.map((e) => e.id).toList(); final assigneeIds = selectedAssignees.map((e) => e.id).toList();
isLoading.value = true;
final success = await ApiService.createServiceProjectJobApi( final success = await ApiService.createServiceProjectJobApi(
title: titleCtrl.text.trim(), title: titleCtrl.text.trim(),
description: descCtrl.text.trim(), description: descCtrl.text.trim(),
projectId: projectId, projectId: projectId,
branchId: selectedBranch.value?.id,
assignees: assigneeIds.map((id) => {"id": id}).toList(), assignees: assigneeIds.map((id) => {"id": id}).toList(),
startDate: startDate.value!, startDate: startDate.value!,
dueDate: dueDate.value!, dueDate: dueDate.value!,
tags: enteredTags.map((tag) => {"name": tag}).toList(), tags: enteredTags.map((tag) => {"name": tag}).toList(),
); );
isLoading.value = false;
if (success) { if (success) {
// 🔥 Auto-refresh job list in ServiceProjectDetailsController
if (Get.isRegistered<ServiceProjectDetailsController>()) { if (Get.isRegistered<ServiceProjectDetailsController>()) {
Get.find<ServiceProjectDetailsController>().refreshJobsAfterAdd(); Get.find<ServiceProjectDetailsController>().refreshJobsAfterAdd();
} }

View File

@ -151,4 +151,5 @@ class ApiEndpoints {
static const String getServiceProjectUpateJobAllocationList = "/serviceproject/get/allocation/list"; static const String getServiceProjectUpateJobAllocationList = "/serviceproject/get/allocation/list";
static const String manageServiceProjectUpateJobAllocation = "/serviceproject/manage/allocation"; static const String manageServiceProjectUpateJobAllocation = "/serviceproject/manage/allocation";
static const String getTeamRoles = "/master/team-roles/list"; static const String getTeamRoles = "/master/team-roles/list";
static const String getServiceProjectBranches = "/serviceproject/branch/list";
} }

View File

@ -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/service_project_job_detail_model.dart';
import 'package:marco/model/service_project/job_attendance_logs_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/job_allocation_model.dart';
import 'package:marco/model/service_project/service_project_branches_model.dart';
class ApiService { class ApiService {
static const bool enableLogs = true; static const bool enableLogs = true;
@ -310,6 +311,51 @@ class ApiService {
} }
} }
/// Fetch Service Project Branches with full response
static Future<ServiceProjectBranchesResponse?> 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 // Service Project Module APIs
static Future<List<TeamRole>?> getTeamRoles() async { static Future<List<TeamRole>?> getTeamRoles() async {
try { try {
@ -558,6 +604,7 @@ class ApiService {
required DateTime startDate, required DateTime startDate,
required DateTime dueDate, required DateTime dueDate,
required List<Map<String, dynamic>> tags, required List<Map<String, dynamic>> tags,
required String? branchId,
}) async { }) async {
const endpoint = ApiEndpoints.createServiceProjectJob; const endpoint = ApiEndpoints.createServiceProjectJob;
logSafe("Creating Service Project Job for projectId: $projectId"); logSafe("Creating Service Project Job for projectId: $projectId");
@ -570,6 +617,7 @@ class ApiService {
"startDate": startDate.toIso8601String(), "startDate": startDate.toIso8601String(),
"dueDate": dueDate.toIso8601String(), "dueDate": dueDate.toIso8601String(),
"tags": tags, "tags": tags,
"branchId": branchId,
}; };
try { try {

View File

@ -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/controller/service_project/add_service_project_job_controller.dart';
import 'package:marco/model/employees/employee_model.dart'; import 'package:marco/model/employees/employee_model.dart';
import 'package:marco/model/employees/multiple_select_bottomsheet.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 { class AddServiceProjectJobBottomSheet extends StatefulWidget {
final String projectId; final String projectId;
@ -31,6 +32,7 @@ class _AddServiceProjectJobBottomSheetState
super.initState(); super.initState();
_selectedEmployees = _selectedEmployees =
RxList<EmployeeModel>.from(controller.selectedAssignees); RxList<EmployeeModel>.from(controller.selectedAssignees);
controller.fetchBranches(widget.projectId);
} }
@override @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<Branch>(
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( Widget _employeeSelector() => Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -203,6 +253,8 @@ class _AddServiceProjectJobBottomSheetState
MySpacing.height(16), MySpacing.height(16),
_employeeSelector(), _employeeSelector(),
MySpacing.height(16), MySpacing.height(16),
_branchSelector(),
MySpacing.height(16),
_labelWithStar("Tags", required: true), _labelWithStar("Tags", required: true),
MySpacing.height(8), MySpacing.height(8),
_tagInput(), _tagInput(),

View File

@ -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<String, dynamic> 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<Branch> data;
Data({
required this.currentPage,
required this.totalPages,
required this.totalEntities,
required this.data,
});
factory Data.fromJson(Map<String, dynamic> json) {
return Data(
currentPage: json['currentPage'] ?? 0,
totalPages: json['totalPages'] ?? 0,
totalEntities: json['totalEntities'] ?? 0,
data: json['data'] != null
? List<Branch>.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<String, dynamic> 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<String, dynamic> 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<String, dynamic> json) {
return CreatedBy(
id: json['id'] ?? '',
firstName: json['firstName'] ?? '',
lastName: json['lastName'] ?? '',
email: json['email'] ?? '',
photo: json['photo'],
jobRoleId: json['jobRoleId'] ?? '',
jobRoleName: json['jobRoleName'] ?? '',
);
}
}