implemented assign employee feature for infra project module
This commit is contained in:
parent
0ac8998c59
commit
55f36fac6d
@ -5,7 +5,6 @@ class ApiEndpoints {
|
||||
// static const String baseUrl = "https://mapi.marcoaiot.com/api";
|
||||
// static const String baseUrl = "https://api.onfieldwork.com/api";
|
||||
|
||||
|
||||
static const String getMasterCurrencies = "/Master/currencies/list";
|
||||
static const String getMasterExpensesCategories =
|
||||
"/Master/expenses-categories";
|
||||
@ -48,7 +47,8 @@ class ApiEndpoints {
|
||||
static const String getProjects = "/project/list";
|
||||
static const String getGlobalProjects = "/project/list/basic";
|
||||
static const String getTodaysAttendance = "/attendance/project/team";
|
||||
static const String getAttendanceForDashboard = "/dashboard/get/attendance/employee/:projectId";
|
||||
static const String getAttendanceForDashboard =
|
||||
"/dashboard/get/attendance/employee/:projectId";
|
||||
static const String getAttendanceLogs = "/attendance/project/log";
|
||||
static const String getAttendanceLogView = "/attendance/log/attendance";
|
||||
static const String getRegularizationLogs = "/attendance/regularize";
|
||||
@ -142,7 +142,6 @@ class ApiEndpoints {
|
||||
static const String manageOrganizationHierarchy =
|
||||
"/organization/hierarchy/manage";
|
||||
|
||||
|
||||
// Service Project Module API Endpoints
|
||||
static const String getServiceProjectsList = "/serviceproject/list";
|
||||
static const String getServiceProjectDetail = "/serviceproject/details";
|
||||
@ -151,10 +150,14 @@ class ApiEndpoints {
|
||||
"/serviceproject/job/details";
|
||||
static const String editServiceProjectJob = "/serviceproject/job/edit";
|
||||
static const String createServiceProjectJob = "/serviceproject/job/create";
|
||||
static const String serviceProjectUpateJobAttendance = "/serviceproject/job/attendance";
|
||||
static const String serviceProjectUpateJobAttendanceLog = "/serviceproject/job/attendance/log";
|
||||
static const String getServiceProjectUpateJobAllocationList = "/serviceproject/get/allocation/list";
|
||||
static const String manageServiceProjectUpateJobAllocation = "/serviceproject/manage/allocation";
|
||||
static const String serviceProjectUpateJobAttendance =
|
||||
"/serviceproject/job/attendance";
|
||||
static const String serviceProjectUpateJobAttendanceLog =
|
||||
"/serviceproject/job/attendance/log";
|
||||
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";
|
||||
|
||||
@ -168,5 +171,5 @@ class ApiEndpoints {
|
||||
static const String getInfraProjectsList = "/project/list";
|
||||
static const String getInfraProjectDetail = "/project/details";
|
||||
static const String getInfraProjectTeamList = "/project/allocation";
|
||||
|
||||
static const String assignInfraProjectAllocation = "/project/allocation";
|
||||
}
|
||||
|
||||
@ -52,6 +52,8 @@ import 'package:on_field_work/model/infra_project/infra_project_details.dart';
|
||||
import 'package:on_field_work/model/dashboard/collection_overview_model.dart';
|
||||
import 'package:on_field_work/model/dashboard/purchase_invoice_model.dart';
|
||||
import 'package:on_field_work/model/infra_project/infra_team_list_model.dart';
|
||||
import 'package:on_field_work/model/infra_project/assign_project_allocation_request.dart';
|
||||
|
||||
|
||||
class ApiService {
|
||||
static const bool enableLogs = true;
|
||||
@ -2008,6 +2010,42 @@ class ApiService {
|
||||
label: "Comment Task", returnFullResponse: true);
|
||||
return parsed != null && parsed['success'] == true;
|
||||
}
|
||||
|
||||
static Future<ProjectAllocationResponse?> assignEmployeesToProject({
|
||||
required List<AssignProjectAllocationRequest> allocations,
|
||||
}) async {
|
||||
if (allocations.isEmpty) {
|
||||
_log(
|
||||
"No allocations provided for assignEmployeesToProject",
|
||||
level: LogLevel.error,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
final endpoint = ApiEndpoints.assignInfraProjectAllocation;
|
||||
final payload = allocations.map((e) => e.toJson()).toList();
|
||||
|
||||
final response = await _safeApiCall(
|
||||
endpoint,
|
||||
method: 'POST',
|
||||
body: payload,
|
||||
);
|
||||
|
||||
if (response == null) return null;
|
||||
|
||||
final parsedJson = _parseAndDecryptResponse(
|
||||
response,
|
||||
label: "AssignInfraProjectAllocation",
|
||||
returnFullResponse: true,
|
||||
);
|
||||
|
||||
if (parsedJson == null || parsedJson is! Map<String, dynamic>) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return ProjectAllocationResponse.fromJson(parsedJson);
|
||||
}
|
||||
|
||||
static Future<ProjectAllocationResponse?> getInfraProjectTeamListApi({
|
||||
required String projectId,
|
||||
String? serviceId,
|
||||
|
||||
@ -0,0 +1,25 @@
|
||||
class AssignProjectAllocationRequest {
|
||||
final String employeeId;
|
||||
final String projectId;
|
||||
final String jobRoleId;
|
||||
final String serviceId;
|
||||
final bool status;
|
||||
|
||||
AssignProjectAllocationRequest({
|
||||
required this.employeeId,
|
||||
required this.projectId,
|
||||
required this.jobRoleId,
|
||||
required this.serviceId,
|
||||
required this.status,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
"employeeId": employeeId,
|
||||
"projectId": projectId,
|
||||
"jobRoleId": jobRoleId,
|
||||
"serviceId": serviceId,
|
||||
"status": status,
|
||||
};
|
||||
}
|
||||
}
|
||||
299
lib/view/infraProject/assign_employee_infra_bottom_sheet.dart
Normal file
299
lib/view/infraProject/assign_employee_infra_bottom_sheet.dart
Normal file
@ -0,0 +1,299 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import 'package:on_field_work/controller/tenant/organization_selection_controller.dart';
|
||||
import 'package:on_field_work/helpers/widgets/my_spacing.dart';
|
||||
import 'package:on_field_work/helpers/widgets/my_text.dart';
|
||||
import 'package:on_field_work/helpers/widgets/tenant/organization_selector.dart';
|
||||
import 'package:on_field_work/model/attendance/organization_per_project_list_model.dart';
|
||||
import 'package:on_field_work/model/employees/employee_model.dart';
|
||||
import 'package:on_field_work/model/employees/multiple_select_bottomsheet.dart';
|
||||
import 'package:on_field_work/controller/tenant/service_controller.dart';
|
||||
import 'package:on_field_work/helpers/widgets/tenant/service_selector.dart';
|
||||
import 'package:on_field_work/model/tenant/tenant_services_model.dart';
|
||||
import 'package:on_field_work/helpers/services/api_service.dart';
|
||||
import 'package:on_field_work/helpers/utils/base_bottom_sheet.dart';
|
||||
import 'package:on_field_work/model/infra_project/assign_project_allocation_request.dart';
|
||||
|
||||
|
||||
class JobRole {
|
||||
final String id;
|
||||
final String name;
|
||||
|
||||
JobRole({required this.id, required this.name});
|
||||
|
||||
factory JobRole.fromJson(Map<String, dynamic> json) {
|
||||
return JobRole(
|
||||
id: json['id'].toString(),
|
||||
name: json['name'] ?? '',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AssignEmployeeBottomSheet extends StatefulWidget {
|
||||
final String projectId;
|
||||
|
||||
const AssignEmployeeBottomSheet({
|
||||
super.key,
|
||||
required this.projectId,
|
||||
});
|
||||
|
||||
@override
|
||||
State<AssignEmployeeBottomSheet> createState() =>
|
||||
_AssignEmployeeBottomSheetState();
|
||||
}
|
||||
|
||||
class _AssignEmployeeBottomSheetState extends State<AssignEmployeeBottomSheet> {
|
||||
late final OrganizationController _organizationController;
|
||||
late final ServiceController _serviceController;
|
||||
|
||||
final RxList<EmployeeModel> _selectedEmployees = <EmployeeModel>[].obs;
|
||||
|
||||
Organization? _selectedOrganization;
|
||||
JobRole? _selectedRole;
|
||||
|
||||
final RxBool _isLoadingRoles = false.obs;
|
||||
final RxList<JobRole> _roles = <JobRole>[].obs;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_organizationController = Get.put(
|
||||
OrganizationController(),
|
||||
tag: 'assign_employee_org',
|
||||
);
|
||||
|
||||
_serviceController = Get.put(
|
||||
ServiceController(),
|
||||
tag: 'assign_employee_service',
|
||||
);
|
||||
|
||||
_organizationController.fetchOrganizations(widget.projectId);
|
||||
_serviceController.fetchServices(widget.projectId);
|
||||
|
||||
_fetchRoles();
|
||||
}
|
||||
|
||||
Future<void> _fetchRoles() async {
|
||||
try {
|
||||
_isLoadingRoles.value = true;
|
||||
final res = await ApiService.getRoles();
|
||||
if (res != null) {
|
||||
_roles.assignAll(
|
||||
res.map((e) => JobRole.fromJson(e)).toList(),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
_isLoadingRoles.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
Get.delete<OrganizationController>(tag: 'assign_employee_org');
|
||||
Get.delete<ServiceController>(tag: 'assign_employee_service');
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _openEmployeeSelector() async {
|
||||
final result = await showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (_) => EmployeeSelectionBottomSheet(
|
||||
title: 'Select Employee(s)',
|
||||
multipleSelection: true,
|
||||
initiallySelected: _selectedEmployees.toList(),
|
||||
),
|
||||
);
|
||||
|
||||
if (result != null && result is List<EmployeeModel>) {
|
||||
_selectedEmployees.assignAll(result);
|
||||
}
|
||||
}
|
||||
|
||||
void _handleAssign() async {
|
||||
if (_selectedEmployees.isEmpty ||
|
||||
_selectedRole == null ||
|
||||
_serviceController.selectedService == null) {
|
||||
Get.snackbar('Error', 'Please complete all selections');
|
||||
return;
|
||||
}
|
||||
|
||||
final allocations = _selectedEmployees
|
||||
.map(
|
||||
(e) => AssignProjectAllocationRequest(
|
||||
employeeId: e.id,
|
||||
projectId: widget.projectId,
|
||||
jobRoleId: _selectedRole!.id,
|
||||
serviceId: _serviceController.selectedService!.id,
|
||||
status: true,
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
|
||||
final res = await ApiService.assignEmployeesToProject(
|
||||
allocations: allocations,
|
||||
);
|
||||
|
||||
if (res?.success == true) {
|
||||
Navigator.of(context).pop(true); // 🔥 triggers refresh
|
||||
} else {
|
||||
Get.snackbar('Error', res?.message ?? 'Assignment failed');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BaseBottomSheet(
|
||||
title: 'Assign Employee',
|
||||
submitText: 'Assign',
|
||||
isSubmitting: false,
|
||||
onCancel: () => Navigator.of(context).pop(),
|
||||
onSubmit: _handleAssign,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
//ORGANIZATION
|
||||
MyText.bodySmall(
|
||||
'Organization',
|
||||
fontWeight: 600,
|
||||
color: Colors.grey.shade700,
|
||||
),
|
||||
MySpacing.height(6),
|
||||
|
||||
OrganizationSelector(
|
||||
controller: _organizationController,
|
||||
height: 44,
|
||||
onSelectionChanged: (Organization? org) async {
|
||||
_selectedOrganization = org;
|
||||
_selectedEmployees.clear();
|
||||
_selectedRole = null;
|
||||
_serviceController.clearSelection();
|
||||
},
|
||||
),
|
||||
|
||||
MySpacing.height(20),
|
||||
|
||||
///EMPLOYEES (SEARCH)
|
||||
MyText.bodySmall(
|
||||
'Employees',
|
||||
fontWeight: 600,
|
||||
color: Colors.grey.shade700,
|
||||
),
|
||||
MySpacing.height(6),
|
||||
|
||||
Obx(
|
||||
() => InkWell(
|
||||
onTap: _openEmployeeSelector,
|
||||
child: _dropdownBox(
|
||||
_selectedEmployees.isEmpty
|
||||
? 'Select employee(s)'
|
||||
: '${_selectedEmployees.length} employee(s) selected',
|
||||
icon: Icons.search,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
MySpacing.height(20),
|
||||
|
||||
///SERVICE
|
||||
MyText.bodySmall(
|
||||
'Service',
|
||||
fontWeight: 600,
|
||||
color: Colors.grey.shade700,
|
||||
),
|
||||
MySpacing.height(6),
|
||||
|
||||
ServiceSelector(
|
||||
controller: _serviceController,
|
||||
height: 44,
|
||||
onSelectionChanged: (Service? service) async {
|
||||
_selectedRole = null;
|
||||
},
|
||||
),
|
||||
|
||||
MySpacing.height(20),
|
||||
|
||||
/// JOB ROLE
|
||||
MyText.bodySmall(
|
||||
'Job Role',
|
||||
fontWeight: 600,
|
||||
color: Colors.grey.shade700,
|
||||
),
|
||||
MySpacing.height(6),
|
||||
|
||||
Obx(() {
|
||||
if (_isLoadingRoles.value) {
|
||||
return _skeleton();
|
||||
}
|
||||
|
||||
return PopupMenuButton<JobRole>(
|
||||
onSelected: (role) {
|
||||
_selectedRole = role;
|
||||
setState(() {});
|
||||
},
|
||||
itemBuilder: (context) {
|
||||
if (_roles.isEmpty) {
|
||||
return const [
|
||||
PopupMenuItem(
|
||||
enabled: false,
|
||||
child: Text('No roles found'),
|
||||
),
|
||||
];
|
||||
}
|
||||
return _roles
|
||||
.map(
|
||||
(r) => PopupMenuItem<JobRole>(
|
||||
value: r,
|
||||
child: Text(r.name),
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
},
|
||||
child: _dropdownBox(
|
||||
_selectedRole?.name ?? 'Select role',
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _dropdownBox(String text, {IconData icon = Icons.arrow_drop_down}) {
|
||||
return Container(
|
||||
height: 44,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: Colors.grey.shade300),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
text,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(fontSize: 13),
|
||||
),
|
||||
),
|
||||
Icon(icon, color: Colors.grey),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _skeleton() {
|
||||
return Container(
|
||||
height: 44,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade300,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -18,6 +18,8 @@ import 'package:on_field_work/controller/infra_project/infra_project_screen_deta
|
||||
import 'package:on_field_work/view/taskPlanning/daily_progress_report.dart';
|
||||
import 'package:on_field_work/view/taskPlanning/daily_task_planning.dart';
|
||||
import 'package:on_field_work/model/infra_project/infra_team_list_model.dart';
|
||||
import 'package:on_field_work/view/infraProject/assign_employee_infra_bottom_sheet.dart';
|
||||
|
||||
|
||||
class InfraProjectDetailsScreen extends StatefulWidget {
|
||||
final String projectId;
|
||||
@ -77,6 +79,21 @@ class _InfraProjectDetailsScreenState extends State<InfraProjectDetailsScreen>
|
||||
_tabController = TabController(length: _tabs.length, vsync: this);
|
||||
}
|
||||
|
||||
void _openAssignEmployeeBottomSheet() async {
|
||||
final result = await showModalBottomSheet<bool>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (_) => AssignEmployeeBottomSheet(
|
||||
projectId: widget.projectId,
|
||||
),
|
||||
);
|
||||
if (result == true) {
|
||||
controller.fetchProjectTeamList();
|
||||
Get.snackbar('Success', 'Employee assigned successfully');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_tabController.dispose();
|
||||
@ -487,6 +504,19 @@ class _InfraProjectDetailsScreenState extends State<InfraProjectDetailsScreen>
|
||||
projectName: widget.projectName,
|
||||
backgroundColor: appBarColor,
|
||||
),
|
||||
floatingActionButton: FloatingActionButton.extended(
|
||||
onPressed: () {
|
||||
_openAssignEmployeeBottomSheet();
|
||||
},
|
||||
backgroundColor: contentTheme.primary,
|
||||
icon: const Icon(Icons.person_add),
|
||||
label: MyText(
|
||||
'Assign Employee',
|
||||
fontSize: 14,
|
||||
color: Colors.white,
|
||||
fontWeight: 500,
|
||||
),
|
||||
),
|
||||
body: Stack(
|
||||
children: [
|
||||
Container(
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user