300 lines
8.3 KiB
Dart
300 lines
8.3 KiB
Dart
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),
|
|
),
|
|
);
|
|
}
|
|
}
|