feat: add branch selection functionality and API integration for service project jobs
This commit is contained in:
parent
bbadcc4139
commit
8edd189479
@ -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?>(DateTime.now());
|
||||
final dueDate = Rx<DateTime?>(DateTime.now().add(const Duration(days: 1)));
|
||||
|
||||
final enteredTags = <String>[].obs;
|
||||
|
||||
final employees = <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 isAllEmployeeLoading = false.obs;
|
||||
final allEmployees = <EmployeeModel>[].obs;
|
||||
final employeeSearchResults = <EmployeeModel>[].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<void> fetchBranches(String projectId) async {
|
||||
isBranchLoading.value = true;
|
||||
|
||||
final response = await ApiService.getServiceProjectBranchesFull(
|
||||
projectId: projectId,
|
||||
);
|
||||
|
||||
if (response != null && response.success) {
|
||||
branches.assignAll(response.data?.data ?? []);
|
||||
}
|
||||
|
||||
/// Create Service Project Job API call
|
||||
isBranchLoading.value = false;
|
||||
}
|
||||
|
||||
// CREATE JOB
|
||||
Future<void> 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<ServiceProjectDetailsController>()) {
|
||||
Get.find<ServiceProjectDetailsController>().refreshJobsAfterAdd();
|
||||
}
|
||||
|
||||
@ -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";
|
||||
}
|
||||
|
||||
@ -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<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
|
||||
static Future<List<TeamRole>?> getTeamRoles() async {
|
||||
try {
|
||||
@ -558,6 +604,7 @@ class ApiService {
|
||||
required DateTime startDate,
|
||||
required DateTime dueDate,
|
||||
required List<Map<String, dynamic>> 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 {
|
||||
|
||||
@ -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<EmployeeModel>.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<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(
|
||||
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(),
|
||||
|
||||
149
lib/model/service_project/service_project_branches_model.dart
Normal file
149
lib/model/service_project/service_project_branches_model.dart
Normal 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'] ?? '',
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user