implemented assign employee feature for infra project module

This commit is contained in:
Manish 2025-12-17 10:37:48 +05:30
parent 0ac8998c59
commit 55f36fac6d
5 changed files with 403 additions and 8 deletions

View File

@ -5,7 +5,6 @@ class ApiEndpoints {
// static const String baseUrl = "https://mapi.marcoaiot.com/api"; // static const String baseUrl = "https://mapi.marcoaiot.com/api";
// static const String baseUrl = "https://api.onfieldwork.com/api"; // static const String baseUrl = "https://api.onfieldwork.com/api";
static const String getMasterCurrencies = "/Master/currencies/list"; static const String getMasterCurrencies = "/Master/currencies/list";
static const String getMasterExpensesCategories = static const String getMasterExpensesCategories =
"/Master/expenses-categories"; "/Master/expenses-categories";
@ -48,7 +47,8 @@ class ApiEndpoints {
static const String getProjects = "/project/list"; static const String getProjects = "/project/list";
static const String getGlobalProjects = "/project/list/basic"; static const String getGlobalProjects = "/project/list/basic";
static const String getTodaysAttendance = "/attendance/project/team"; 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 getAttendanceLogs = "/attendance/project/log";
static const String getAttendanceLogView = "/attendance/log/attendance"; static const String getAttendanceLogView = "/attendance/log/attendance";
static const String getRegularizationLogs = "/attendance/regularize"; static const String getRegularizationLogs = "/attendance/regularize";
@ -142,7 +142,6 @@ class ApiEndpoints {
static const String manageOrganizationHierarchy = static const String manageOrganizationHierarchy =
"/organization/hierarchy/manage"; "/organization/hierarchy/manage";
// Service Project Module API Endpoints // Service Project Module API Endpoints
static const String getServiceProjectsList = "/serviceproject/list"; static const String getServiceProjectsList = "/serviceproject/list";
static const String getServiceProjectDetail = "/serviceproject/details"; static const String getServiceProjectDetail = "/serviceproject/details";
@ -151,10 +150,14 @@ class ApiEndpoints {
"/serviceproject/job/details"; "/serviceproject/job/details";
static const String editServiceProjectJob = "/serviceproject/job/edit"; static const String editServiceProjectJob = "/serviceproject/job/edit";
static const String createServiceProjectJob = "/serviceproject/job/create"; static const String createServiceProjectJob = "/serviceproject/job/create";
static const String serviceProjectUpateJobAttendance = "/serviceproject/job/attendance"; static const String serviceProjectUpateJobAttendance =
static const String serviceProjectUpateJobAttendanceLog = "/serviceproject/job/attendance/log"; "/serviceproject/job/attendance";
static const String getServiceProjectUpateJobAllocationList = "/serviceproject/get/allocation/list"; static const String serviceProjectUpateJobAttendanceLog =
static const String manageServiceProjectUpateJobAllocation = "/serviceproject/manage/allocation"; "/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 getTeamRoles = "/master/team-roles/list";
static const String getServiceProjectBranches = "/serviceproject/branch/list"; static const String getServiceProjectBranches = "/serviceproject/branch/list";
@ -168,5 +171,5 @@ class ApiEndpoints {
static const String getInfraProjectsList = "/project/list"; static const String getInfraProjectsList = "/project/list";
static const String getInfraProjectDetail = "/project/details"; static const String getInfraProjectDetail = "/project/details";
static const String getInfraProjectTeamList = "/project/allocation"; static const String getInfraProjectTeamList = "/project/allocation";
static const String assignInfraProjectAllocation = "/project/allocation";
} }

View File

@ -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/collection_overview_model.dart';
import 'package:on_field_work/model/dashboard/purchase_invoice_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/infra_team_list_model.dart';
import 'package:on_field_work/model/infra_project/assign_project_allocation_request.dart';
class ApiService { class ApiService {
static const bool enableLogs = true; static const bool enableLogs = true;
@ -2008,6 +2010,42 @@ class ApiService {
label: "Comment Task", returnFullResponse: true); label: "Comment Task", returnFullResponse: true);
return parsed != null && parsed['success'] == 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({ static Future<ProjectAllocationResponse?> getInfraProjectTeamListApi({
required String projectId, required String projectId,
String? serviceId, String? serviceId,

View File

@ -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,
};
}
}

View 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),
),
);
}
}

View File

@ -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_progress_report.dart';
import 'package:on_field_work/view/taskPlanning/daily_task_planning.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/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 { class InfraProjectDetailsScreen extends StatefulWidget {
final String projectId; final String projectId;
@ -77,6 +79,21 @@ class _InfraProjectDetailsScreenState extends State<InfraProjectDetailsScreen>
_tabController = TabController(length: _tabs.length, vsync: this); _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 @override
void dispose() { void dispose() {
_tabController.dispose(); _tabController.dispose();
@ -487,6 +504,19 @@ class _InfraProjectDetailsScreenState extends State<InfraProjectDetailsScreen>
projectName: widget.projectName, projectName: widget.projectName,
backgroundColor: appBarColor, 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( body: Stack(
children: [ children: [
Container( Container(