feat: Enhance AddEmployee functionality with email and organization selection support

This commit is contained in:
Vaibhav Surve 2025-09-27 16:56:56 +05:30
parent 075167e285
commit 8576448a32
4 changed files with 570 additions and 333 deletions

View File

@ -1,13 +1,13 @@
import 'package:file_picker/file_picker.dart'; import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:marco/controller/my_controller.dart'; import 'package:marco/controller/my_controller.dart';
import 'package:marco/helpers/widgets/my_form_validator.dart';
import 'package:marco/helpers/services/api_service.dart'; import 'package:marco/helpers/services/api_service.dart';
import 'package:marco/helpers/services/app_logger.dart';
import 'package:marco/helpers/widgets/my_form_validator.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart'; import 'package:marco/helpers/widgets/my_snackbar.dart';
import 'package:flutter_contacts/flutter_contacts.dart'; import 'package:flutter_contacts/flutter_contacts.dart';
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
import 'package:marco/helpers/services/app_logger.dart';
import 'package:collection/collection.dart';
enum Gender { enum Gender {
male, male,
@ -18,22 +18,26 @@ enum Gender {
} }
class AddEmployeeController extends MyController { class AddEmployeeController extends MyController {
Map<String, dynamic>? editingEmployeeData; // For edit mode Map<String, dynamic>? editingEmployeeData;
List<PlatformFile> files = []; // State
final MyFormValidator basicValidator = MyFormValidator(); final MyFormValidator basicValidator = MyFormValidator();
final List<PlatformFile> files = [];
final List<String> categories = [];
Gender? selectedGender; Gender? selectedGender;
List<Map<String, dynamic>> roles = []; List<Map<String, dynamic>> roles = [];
String? selectedRoleId; String? selectedRoleId;
String selectedCountryCode = "+91"; String selectedCountryCode = '+91';
bool showOnline = true; bool showOnline = true;
final List<String> categories = [];
DateTime? joiningDate; DateTime? joiningDate;
String? selectedOrganizationId;
RxString selectedOrganizationName = RxString('');
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
logSafe("Initializing AddEmployeeController..."); logSafe('Initializing AddEmployeeController...');
_initializeFields(); _initializeFields();
fetchRoles(); fetchRoles();
@ -45,29 +49,36 @@ class AddEmployeeController extends MyController {
void _initializeFields() { void _initializeFields() {
basicValidator.addField( basicValidator.addField(
'first_name', 'first_name',
label: "First Name", label: 'First Name',
required: true, required: true,
controller: TextEditingController(), controller: TextEditingController(),
); );
basicValidator.addField( basicValidator.addField(
'phone_number', 'phone_number',
label: "Phone Number", label: 'Phone Number',
required: true, required: true,
controller: TextEditingController(), controller: TextEditingController(),
); );
basicValidator.addField( basicValidator.addField(
'last_name', 'last_name',
label: "Last Name", label: 'Last Name',
required: true, required: true,
controller: TextEditingController(), controller: TextEditingController(),
); );
logSafe("Fields initialized for first_name, phone_number, last_name."); // Email is optional in controller; UI enforces when application access is checked
basicValidator.addField(
'email',
label: 'Email',
required: false,
controller: TextEditingController(),
);
logSafe('Fields initialized for first_name, phone_number, last_name, email.');
} }
/// Prefill fields in edit mode // Prefill fields in edit mode
// In AddEmployeeController
void prefillFields() { void prefillFields() {
logSafe("Prefilling data for editing..."); logSafe('Prefilling data for editing...');
basicValidator.getController('first_name')?.text = basicValidator.getController('first_name')?.text =
editingEmployeeData?['first_name'] ?? ''; editingEmployeeData?['first_name'] ?? '';
basicValidator.getController('last_name')?.text = basicValidator.getController('last_name')?.text =
@ -76,10 +87,12 @@ class AddEmployeeController extends MyController {
editingEmployeeData?['phone_number'] ?? ''; editingEmployeeData?['phone_number'] ?? '';
selectedGender = editingEmployeeData?['gender'] != null selectedGender = editingEmployeeData?['gender'] != null
? Gender.values ? Gender.values.firstWhereOrNull((g) => g.name == editingEmployeeData!['gender'])
.firstWhereOrNull((g) => g.name == editingEmployeeData!['gender'])
: null; : null;
basicValidator.getController('email')?.text =
editingEmployeeData?['email'] ?? '';
selectedRoleId = editingEmployeeData?['job_role_id']; selectedRoleId = editingEmployeeData?['job_role_id'];
if (editingEmployeeData?['joining_date'] != null) { if (editingEmployeeData?['joining_date'] != null) {
@ -91,92 +104,102 @@ class AddEmployeeController extends MyController {
void setJoiningDate(DateTime date) { void setJoiningDate(DateTime date) {
joiningDate = date; joiningDate = date;
logSafe("Joining date selected: $date"); logSafe('Joining date selected: $date');
update(); update();
} }
void onGenderSelected(Gender? gender) { void onGenderSelected(Gender? gender) {
selectedGender = gender; selectedGender = gender;
logSafe("Gender selected: ${gender?.name}"); logSafe('Gender selected: ${gender?.name}');
update(); update();
} }
Future<void> fetchRoles() async { Future<void> fetchRoles() async {
logSafe("Fetching roles..."); logSafe('Fetching roles...');
try { try {
final result = await ApiService.getRoles(); final result = await ApiService.getRoles();
if (result != null) { if (result != null) {
roles = List<Map<String, dynamic>>.from(result); roles = List<Map<String, dynamic>>.from(result);
logSafe("Roles fetched successfully."); logSafe('Roles fetched successfully.');
update(); update();
} else { } else {
logSafe("Failed to fetch roles: null result", level: LogLevel.error); logSafe('Failed to fetch roles: null result', level: LogLevel.error);
} }
} catch (e, st) { } catch (e, st) {
logSafe("Error fetching roles", logSafe('Error fetching roles', level: LogLevel.error, error: e, stackTrace: st);
level: LogLevel.error, error: e, stackTrace: st);
} }
} }
void onRoleSelected(String? roleId) { void onRoleSelected(String? roleId) {
selectedRoleId = roleId; selectedRoleId = roleId;
logSafe("Role selected: $roleId"); logSafe('Role selected: $roleId');
update(); update();
} }
/// Create or update employee // Create or update employee
Future<Map<String, dynamic>?> createOrUpdateEmployee() async { Future<Map<String, dynamic>?> createOrUpdateEmployee({
String? email,
bool hasApplicationAccess = false,
}) async {
logSafe(editingEmployeeData != null logSafe(editingEmployeeData != null
? "Starting employee update..." ? 'Starting employee update...'
: "Starting employee creation..."); : 'Starting employee creation...');
if (selectedGender == null || selectedRoleId == null) { if (selectedGender == null || selectedRoleId == null) {
showAppSnackbar( showAppSnackbar(
title: "Missing Fields", title: 'Missing Fields',
message: "Please select both Gender and Role.", message: 'Please select both Gender and Role.',
type: SnackbarType.warning, type: SnackbarType.warning,
); );
return null; return null;
} }
final firstName = basicValidator.getController("first_name")?.text.trim(); final firstName = basicValidator.getController('first_name')?.text.trim();
final lastName = basicValidator.getController("last_name")?.text.trim(); final lastName = basicValidator.getController('last_name')?.text.trim();
final phoneNumber = final phoneNumber = basicValidator.getController('phone_number')?.text.trim();
basicValidator.getController("phone_number")?.text.trim();
try { try {
// sanitize orgId before sending
final String? orgId = (selectedOrganizationId != null &&
selectedOrganizationId!.trim().isNotEmpty)
? selectedOrganizationId
: null;
final response = await ApiService.createEmployee( final response = await ApiService.createEmployee(
id: editingEmployeeData?['id'], // Pass id if editing id: editingEmployeeData?['id'],
firstName: firstName!, firstName: firstName!,
lastName: lastName!, lastName: lastName!,
phoneNumber: phoneNumber!, phoneNumber: phoneNumber!,
gender: selectedGender!.name, gender: selectedGender!.name,
jobRoleId: selectedRoleId!, jobRoleId: selectedRoleId!,
joiningDate: joiningDate?.toIso8601String() ?? "", joiningDate: joiningDate?.toIso8601String() ?? '',
organizationId: orgId,
email: email,
hasApplicationAccess: hasApplicationAccess,
); );
logSafe("Response: $response"); logSafe('Response: $response');
if (response != null && response['success'] == true) { if (response != null && response['success'] == true) {
showAppSnackbar( showAppSnackbar(
title: "Success", title: 'Success',
message: editingEmployeeData != null message: editingEmployeeData != null
? "Employee updated successfully!" ? 'Employee updated successfully!'
: "Employee created successfully!", : 'Employee created successfully!',
type: SnackbarType.success, type: SnackbarType.success,
); );
return response; return response;
} else { } else {
logSafe("Failed operation", level: LogLevel.error); logSafe('Failed operation', level: LogLevel.error);
} }
} catch (e, st) { } catch (e, st) {
logSafe("Error creating/updating employee", logSafe('Error creating/updating employee',
level: LogLevel.error, error: e, stackTrace: st); level: LogLevel.error, error: e, stackTrace: st);
} }
showAppSnackbar( showAppSnackbar(
title: "Error", title: 'Error',
message: "Failed to save employee.", message: 'Failed to save employee.',
type: SnackbarType.error, type: SnackbarType.error,
); );
return null; return null;
@ -192,9 +215,8 @@ class AddEmployeeController extends MyController {
} }
showAppSnackbar( showAppSnackbar(
title: "Permission Required", title: 'Permission Required',
message: message: 'Please allow Contacts permission from settings to pick a contact.',
"Please allow Contacts permission from settings to pick a contact.",
type: SnackbarType.warning, type: SnackbarType.warning,
); );
return false; return false;
@ -212,8 +234,8 @@ class AddEmployeeController extends MyController {
await FlutterContacts.getContact(picked.id, withProperties: true); await FlutterContacts.getContact(picked.id, withProperties: true);
if (contact == null) { if (contact == null) {
showAppSnackbar( showAppSnackbar(
title: "Error", title: 'Error',
message: "Failed to load contact details.", message: 'Failed to load contact details.',
type: SnackbarType.error, type: SnackbarType.error,
); );
return; return;
@ -221,8 +243,8 @@ class AddEmployeeController extends MyController {
if (contact.phones.isEmpty) { if (contact.phones.isEmpty) {
showAppSnackbar( showAppSnackbar(
title: "No Phone Number", title: 'No Phone Number',
message: "Selected contact has no phone number.", message: 'Selected contact has no phone number.',
type: SnackbarType.warning, type: SnackbarType.warning,
); );
return; return;
@ -236,8 +258,8 @@ class AddEmployeeController extends MyController {
if (indiaPhones.isEmpty) { if (indiaPhones.isEmpty) {
showAppSnackbar( showAppSnackbar(
title: "No Indian Number", title: 'No Indian Number',
message: "Selected contact has no Indian (+91) phone number.", message: 'Selected contact has no Indian (+91) phone number.',
type: SnackbarType.warning, type: SnackbarType.warning,
); );
return; return;
@ -250,19 +272,20 @@ class AddEmployeeController extends MyController {
selectedPhone = await showDialog<String>( selectedPhone = await showDialog<String>(
context: context, context: context,
builder: (ctx) => AlertDialog( builder: (ctx) => AlertDialog(
title: Text("Choose an Indian number"), title: const Text('Choose an Indian number'),
content: Column( content: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: indiaPhones children: indiaPhones
.map((p) => ListTile( .map(
title: Text(p.number), (p) => ListTile(
onTap: () => Navigator.of(ctx).pop(p.number), title: Text(p.number),
)) onTap: () => Navigator.of(ctx).pop(p.number),
),
)
.toList(), .toList(),
), ),
), ),
); );
if (selectedPhone == null) return; if (selectedPhone == null) return;
} }
@ -275,11 +298,11 @@ class AddEmployeeController extends MyController {
phoneWithoutCountryCode; phoneWithoutCountryCode;
update(); update();
} catch (e, st) { } catch (e, st) {
logSafe("Error fetching contacts", logSafe('Error fetching contacts',
level: LogLevel.error, error: e, stackTrace: st); level: LogLevel.error, error: e, stackTrace: st);
showAppSnackbar( showAppSnackbar(
title: "Error", title: 'Error',
message: "Failed to fetch contacts.", message: 'Failed to fetch contacts.',
type: SnackbarType.error, type: SnackbarType.error,
); );
} }

View File

@ -25,7 +25,7 @@ class ApiEndpoints {
static const String getAllEmployees = "/employee/list"; static const String getAllEmployees = "/employee/list";
static const String getEmployeesWithoutPermission = "/employee/basic"; static const String getEmployeesWithoutPermission = "/employee/basic";
static const String getRoles = "/roles/jobrole"; static const String getRoles = "/roles/jobrole";
static const String createEmployee = "/employee/manage-mobile"; static const String createEmployee = "/employee/app/manage";
static const String getEmployeeInfo = "/employee/profile/get"; static const String getEmployeeInfo = "/employee/profile/get";
static const String assignEmployee = "/employee/profile/get"; static const String assignEmployee = "/employee/profile/get";
static const String getAssignedProjects = "/project/assigned-projects"; static const String getAssignedProjects = "/project/assigned-projects";

View File

@ -1976,39 +1976,46 @@ class ApiService {
static Future<List<dynamic>?> getRoles() async => static Future<List<dynamic>?> getRoles() async =>
_getRequest(ApiEndpoints.getRoles).then( _getRequest(ApiEndpoints.getRoles).then(
(res) => res != null ? _parseResponse(res, label: 'Roles') : null); (res) => res != null ? _parseResponse(res, label: 'Roles') : null);
static Future<Map<String, dynamic>?> createEmployee({ static Future<Map<String, dynamic>?> createEmployee({
String? id, String? id,
required String firstName, required String firstName,
required String lastName, required String lastName,
required String phoneNumber, required String phoneNumber,
required String gender, required String gender,
required String jobRoleId, required String jobRoleId,
required String joiningDate, required String joiningDate,
}) async { String? email,
final body = { String? organizationId,
if (id != null) "id": id, bool? hasApplicationAccess,
"firstName": firstName, }) async {
"lastName": lastName, final body = {
"phoneNumber": phoneNumber, if (id != null) "id": id,
"gender": gender, "firstName": firstName,
"jobRoleId": jobRoleId, "lastName": lastName,
"joiningDate": joiningDate, "phoneNumber": phoneNumber,
}; "gender": gender,
"jobRoleId": jobRoleId,
"joiningDate": joiningDate,
if (email != null && email.isNotEmpty) "email": email,
if (organizationId != null && organizationId.isNotEmpty)
"organizationId": organizationId,
if (hasApplicationAccess != null) "hasApplicationAccess": hasApplicationAccess,
};
final response = await _postRequest( final response = await _postRequest(
ApiEndpoints.createEmployee, ApiEndpoints.createEmployee,
body, body,
customTimeout: extendedTimeout, customTimeout: extendedTimeout,
); );
if (response == null) return null; if (response == null) return null;
final json = jsonDecode(response.body); final json = jsonDecode(response.body);
return { return {
"success": response.statusCode == 200 && json['success'] == true, "success": response.statusCode == 200 && json['success'] == true,
"data": json, "data": json,
}; };
} }
static Future<Map<String, dynamic>?> getEmployeeDetails( static Future<Map<String, dynamic>?> getEmployeeDetails(
String employeeId) async { String employeeId) async {

View File

@ -1,19 +1,21 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:intl/intl.dart';
import 'package:marco/controller/employee/add_employee_controller.dart'; import 'package:marco/controller/employee/add_employee_controller.dart';
import 'package:marco/controller/employee/employees_screen_controller.dart'; import 'package:marco/controller/employee/employees_screen_controller.dart';
import 'package:marco/controller/tenant/organization_selection_controller.dart';
import 'package:marco/helpers/utils/base_bottom_sheet.dart';
import 'package:marco/helpers/utils/mixins/ui_mixin.dart'; import 'package:marco/helpers/utils/mixins/ui_mixin.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart';
import 'package:marco/helpers/widgets/my_spacing.dart'; import 'package:marco/helpers/widgets/my_spacing.dart';
import 'package:marco/helpers/widgets/my_text.dart'; import 'package:marco/helpers/widgets/my_text.dart';
import 'package:marco/helpers/widgets/my_text_style.dart'; import 'package:marco/helpers/widgets/my_text_style.dart';
import 'package:marco/helpers/utils/base_bottom_sheet.dart';
import 'package:intl/intl.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart';
class AddEmployeeBottomSheet extends StatefulWidget { class AddEmployeeBottomSheet extends StatefulWidget {
final Map<String, dynamic>? employeeData; final Map<String, dynamic>? employeeData;
AddEmployeeBottomSheet({this.employeeData}); const AddEmployeeBottomSheet({super.key, this.employeeData});
@override @override
State<AddEmployeeBottomSheet> createState() => _AddEmployeeBottomSheetState(); State<AddEmployeeBottomSheet> createState() => _AddEmployeeBottomSheetState();
@ -22,28 +24,88 @@ class AddEmployeeBottomSheet extends StatefulWidget {
class _AddEmployeeBottomSheetState extends State<AddEmployeeBottomSheet> class _AddEmployeeBottomSheetState extends State<AddEmployeeBottomSheet>
with UIMixin { with UIMixin {
late final AddEmployeeController _controller; late final AddEmployeeController _controller;
final OrganizationController _organizationController =
Get.put(OrganizationController());
// Local UI state
bool _hasApplicationAccess = false;
// Local read-only controllers to avoid recreating TextEditingController in build
late final TextEditingController _orgFieldController;
late final TextEditingController _joiningDateController;
late final TextEditingController _genderController;
late final TextEditingController _roleController;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_controller = Get.put( _controller = Get.put(
AddEmployeeController(), AddEmployeeController(),
tag: UniqueKey().toString(), // Unique tag to avoid clashes, but stable for this widget instance
tag: UniqueKey().toString(),
); );
_orgFieldController = TextEditingController(text: '');
_joiningDateController = TextEditingController(text: '');
_genderController = TextEditingController(text: '');
_roleController = TextEditingController(text: '');
// Prefill when editing
if (widget.employeeData != null) { if (widget.employeeData != null) {
_controller.editingEmployeeData = widget.employeeData; _controller.editingEmployeeData = widget.employeeData;
_controller.prefillFields(); _controller.prefillFields();
final orgId = widget.employeeData!['organizationId'];
if (orgId != null) {
_controller.selectedOrganizationId = orgId;
final selectedOrg = _organizationController.organizations
.firstWhereOrNull((o) => o.id == orgId);
if (selectedOrg != null) {
_organizationController.selectOrganization(selectedOrg);
_orgFieldController.text = selectedOrg.name;
}
}
if (_controller.joiningDate != null) {
_joiningDateController.text =
DateFormat('dd MMM yyyy').format(_controller.joiningDate!);
}
if (_controller.selectedGender != null) {
_genderController.text =
_controller.selectedGender!.name.capitalizeFirst ?? '';
}
final roleName = _controller.roles.firstWhereOrNull(
(r) => r['id'] == _controller.selectedRoleId)?['name'] ??
'';
_roleController.text = roleName;
} else {
_orgFieldController.text = _organizationController.currentSelection;
} }
} }
@override
void dispose() {
_orgFieldController.dispose();
_joiningDateController.dispose();
_genderController.dispose();
_roleController.dispose();
super.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return GetBuilder<AddEmployeeController>( return GetBuilder<AddEmployeeController>(
init: _controller, init: _controller,
builder: (_) { builder: (_) {
// Keep org field in sync with controller selection
_orgFieldController.text = _organizationController.currentSelection;
return BaseBottomSheet( return BaseBottomSheet(
title: widget.employeeData != null ? "Edit Employee" : "Add Employee", title: widget.employeeData != null ? 'Edit Employee' : 'Add Employee',
onCancel: () => Navigator.pop(context), onCancel: () => Navigator.pop(context),
onSubmit: _handleSubmit, onSubmit: _handleSubmit,
child: Form( child: Form(
@ -51,11 +113,11 @@ class _AddEmployeeBottomSheetState extends State<AddEmployeeBottomSheet>
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
_sectionLabel("Personal Info"), _sectionLabel('Personal Info'),
MySpacing.height(16), MySpacing.height(16),
_inputWithIcon( _inputWithIcon(
label: "First Name", label: 'First Name',
hint: "e.g., John", hint: 'e.g., John',
icon: Icons.person, icon: Icons.person,
controller: controller:
_controller.basicValidator.getController('first_name')!, _controller.basicValidator.getController('first_name')!,
@ -64,8 +126,8 @@ class _AddEmployeeBottomSheetState extends State<AddEmployeeBottomSheet>
), ),
MySpacing.height(16), MySpacing.height(16),
_inputWithIcon( _inputWithIcon(
label: "Last Name", label: 'Last Name',
hint: "e.g., Doe", hint: 'e.g., Doe',
icon: Icons.person_outline, icon: Icons.person_outline,
controller: controller:
_controller.basicValidator.getController('last_name')!, _controller.basicValidator.getController('last_name')!,
@ -73,37 +135,91 @@ class _AddEmployeeBottomSheetState extends State<AddEmployeeBottomSheet>
_controller.basicValidator.getValidation('last_name'), _controller.basicValidator.getValidation('last_name'),
), ),
MySpacing.height(16), MySpacing.height(16),
_sectionLabel("Joining Details"), _sectionLabel('Organization'),
MySpacing.height(8),
GestureDetector(
onTap: () => _showOrganizationPopup(context),
child: AbsorbPointer(
child: TextFormField(
readOnly: true,
controller: _orgFieldController,
validator: (val) {
if (val == null ||
val.trim().isEmpty ||
val == 'All Organizations') {
return 'Organization is required';
}
return null;
},
decoration:
_inputDecoration('Select Organization').copyWith(
suffixIcon: const Icon(Icons.expand_more),
),
),
),
),
MySpacing.height(24),
_sectionLabel('Application Access'),
Row(
children: [
Checkbox(
value: _hasApplicationAccess,
onChanged: (val) {
setState(() => _hasApplicationAccess = val ?? false);
},
fillColor:
WidgetStateProperty.resolveWith<Color>((states) {
if (states.contains(WidgetState.selected)) {
return Colors.indigo;
}
return Colors.white;
}),
side: WidgetStateBorderSide.resolveWith((states) {
if (states.contains(WidgetState.selected)) {
return BorderSide.none;
}
return const BorderSide(
color: Colors.black,
width: 2,
);
}),
checkColor: Colors.white,
),
MyText.bodyMedium(
'Has Application Access',
fontWeight: 600,
),
],
),
MySpacing.height(8),
_buildEmailField(),
MySpacing.height(12),
_sectionLabel('Joining Details'),
MySpacing.height(16), MySpacing.height(16),
_buildDatePickerField( _buildDatePickerField(
label: "Joining Date", label: 'Joining Date',
value: _controller.joiningDate != null controller: _joiningDateController,
? DateFormat("dd MMM yyyy") hint: 'Select Joining Date',
.format(_controller.joiningDate!)
: "",
hint: "Select Joining Date",
onTap: () => _pickJoiningDate(context), onTap: () => _pickJoiningDate(context),
), ),
MySpacing.height(16), MySpacing.height(16),
_sectionLabel("Contact Details"), _sectionLabel('Contact Details'),
MySpacing.height(16), MySpacing.height(16),
_buildPhoneInput(context), _buildPhoneInput(context),
MySpacing.height(24), MySpacing.height(24),
_sectionLabel("Other Details"), _sectionLabel('Other Details'),
MySpacing.height(16), MySpacing.height(16),
_buildDropdownField( _buildDropdownField(
label: "Gender", label: 'Gender',
value: _controller.selectedGender?.name.capitalizeFirst ?? '', controller: _genderController,
hint: "Select Gender", hint: 'Select Gender',
onTap: () => _showGenderPopup(context), onTap: () => _showGenderPopup(context),
), ),
MySpacing.height(16), MySpacing.height(16),
_buildDropdownField( _buildDropdownField(
label: "Role", label: 'Role',
value: _controller.roles.firstWhereOrNull((role) => controller: _roleController,
role['id'] == _controller.selectedRoleId)?['name'] ?? hint: 'Select Role',
"",
hint: "Select Role",
onTap: () => _showRolePopup(context), onTap: () => _showRolePopup(context),
), ),
], ],
@ -114,96 +230,7 @@ class _AddEmployeeBottomSheetState extends State<AddEmployeeBottomSheet>
); );
} }
Widget _requiredLabel(String text) { // UI Pieces
return Row(
children: [
MyText.labelMedium(text),
const SizedBox(width: 4),
const Text("*", style: TextStyle(color: Colors.red)),
],
);
}
Widget _buildDatePickerField({
required String label,
required String value,
required String hint,
required VoidCallback onTap,
}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_requiredLabel(label),
MySpacing.height(8),
GestureDetector(
onTap: onTap,
child: AbsorbPointer(
child: TextFormField(
readOnly: true,
controller: TextEditingController(text: value),
validator: (val) {
if (val == null || val.trim().isEmpty) {
return "$label is required";
}
return null;
},
decoration: _inputDecoration(hint).copyWith(
suffixIcon: const Icon(Icons.calendar_today),
),
),
),
),
],
);
}
Future<void> _pickJoiningDate(BuildContext context) async {
final picked = await showDatePicker(
context: context,
initialDate: _controller.joiningDate ?? DateTime.now(),
firstDate: DateTime(2000),
lastDate: DateTime(2100),
);
if (picked != null) {
_controller.setJoiningDate(picked);
_controller.update();
}
}
Future<void> _handleSubmit() async {
final isValid =
_controller.basicValidator.formKey.currentState?.validate() ?? false;
if (!isValid ||
_controller.joiningDate == null ||
_controller.selectedGender == null ||
_controller.selectedRoleId == null) {
showAppSnackbar(
title: "Missing Fields",
message: "Please complete all required fields.",
type: SnackbarType.warning,
);
return;
}
final result = await _controller.createOrUpdateEmployee();
if (result != null && result['success'] == true) {
final employeeController = Get.find<EmployeesScreenController>();
final projectId = employeeController.selectedProjectId;
if (projectId == null) {
await employeeController.fetchAllEmployees();
} else {
await employeeController.fetchEmployeesByProject(projectId);
}
employeeController.update(['employee_screen_controller']);
Navigator.pop(context, result['data']);
}
}
Widget _sectionLabel(String title) => Column( Widget _sectionLabel(String title) => Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -214,116 +241,12 @@ class _AddEmployeeBottomSheetState extends State<AddEmployeeBottomSheet>
], ],
); );
Widget _inputWithIcon({ Widget _requiredLabel(String text) {
required String label, return Row(
required String hint,
required IconData icon,
required TextEditingController controller,
required String? Function(String?)? validator,
}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
_requiredLabel(label), MyText.labelMedium(text),
MySpacing.height(8), const SizedBox(width: 4),
TextFormField( const Text('*', style: TextStyle(color: Colors.red)),
controller: controller,
validator: (val) {
if (val == null || val.trim().isEmpty) {
return "$label is required";
}
return validator?.call(val);
},
decoration: _inputDecoration(hint).copyWith(
prefixIcon: Icon(icon, size: 20),
),
),
],
);
}
Widget _buildPhoneInput(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_requiredLabel("Phone Number"),
MySpacing.height(8),
Row(
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 14),
decoration: BoxDecoration(
border: Border.all(color: Colors.grey.shade300),
borderRadius: BorderRadius.circular(12),
color: Colors.grey.shade100,
),
child: const Text("+91"),
),
MySpacing.width(12),
Expanded(
child: TextFormField(
controller:
_controller.basicValidator.getController('phone_number'),
validator: (value) {
if (value == null || value.trim().isEmpty) {
return "Phone Number is required";
}
if (value.trim().length != 10) {
return "Phone Number must be exactly 10 digits";
}
if (!RegExp(r'^\d{10}$').hasMatch(value.trim())) {
return "Enter a valid 10-digit number";
}
return null;
},
keyboardType: TextInputType.number,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
LengthLimitingTextInputFormatter(10),
],
decoration: _inputDecoration("e.g., 9876543210").copyWith(
suffixIcon: IconButton(
icon: const Icon(Icons.contacts),
onPressed: () => _controller.pickContact(context),
),
),
),
),
],
),
],
);
}
Widget _buildDropdownField({
required String label,
required String value,
required String hint,
required VoidCallback onTap,
}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_requiredLabel(label),
MySpacing.height(8),
GestureDetector(
onTap: onTap,
child: AbsorbPointer(
child: TextFormField(
readOnly: true,
controller: TextEditingController(text: value),
validator: (val) {
if (val == null || val.trim().isEmpty) {
return "$label is required";
}
return null;
},
decoration: _inputDecoration(hint).copyWith(
suffixIcon: const Icon(Icons.expand_more),
),
),
),
),
], ],
); );
} }
@ -350,20 +273,298 @@ class _AddEmployeeBottomSheetState extends State<AddEmployeeBottomSheet>
); );
} }
Widget _inputWithIcon({
required String label,
required String hint,
required IconData icon,
required TextEditingController controller,
required String? Function(String?)? validator,
}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_requiredLabel(label),
MySpacing.height(8),
TextFormField(
controller: controller,
validator: (val) {
if (val == null || val.trim().isEmpty) {
return '$label is required';
}
return validator?.call(val);
},
decoration: _inputDecoration(hint).copyWith(
prefixIcon: Icon(icon, size: 20),
),
),
],
);
}
Widget _buildEmailField() {
final emailController = _controller.basicValidator.getController('email') ??
TextEditingController();
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
MyText.labelMedium('Email'),
const SizedBox(width: 4),
if (_hasApplicationAccess)
const Text('*', style: TextStyle(color: Colors.red)),
],
),
MySpacing.height(8),
TextFormField(
controller: emailController,
validator: (val) {
if (_hasApplicationAccess) {
if (val == null || val.trim().isEmpty) {
return 'Email is required for application users';
}
final email = val.trim();
if (!RegExp(r'^[\w\-\.]+@([\w\-]+\.)+[\w\-]{2,4}$')
.hasMatch(email)) {
return 'Enter a valid email address';
}
}
return null;
},
keyboardType: TextInputType.emailAddress,
decoration: _inputDecoration('e.g., john.doe@example.com').copyWith(
),
),
],
);
}
Widget _buildDatePickerField({
required String label,
required TextEditingController controller,
required String hint,
required VoidCallback onTap,
}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_requiredLabel(label),
MySpacing.height(8),
GestureDetector(
onTap: onTap,
child: AbsorbPointer(
child: TextFormField(
readOnly: true,
controller: controller,
validator: (val) {
if (val == null || val.trim().isEmpty) {
return '$label is required';
}
return null;
},
decoration: _inputDecoration(hint).copyWith(
suffixIcon: const Icon(Icons.calendar_today),
),
),
),
),
],
);
}
Widget _buildDropdownField({
required String label,
required TextEditingController controller,
required String hint,
required VoidCallback onTap,
}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_requiredLabel(label),
MySpacing.height(8),
GestureDetector(
onTap: onTap,
child: AbsorbPointer(
child: TextFormField(
readOnly: true,
controller: controller,
validator: (val) {
if (val == null || val.trim().isEmpty) {
return '$label is required';
}
return null;
},
decoration: _inputDecoration(hint).copyWith(
suffixIcon: const Icon(Icons.expand_more),
),
),
),
),
],
);
}
Widget _buildPhoneInput(BuildContext context) {
final phoneController =
_controller.basicValidator.getController('phone_number');
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_requiredLabel('Phone Number'),
MySpacing.height(8),
Row(
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 14),
decoration: BoxDecoration(
border: Border.all(color: Colors.grey.shade300),
borderRadius: BorderRadius.circular(12),
color: Colors.grey.shade100,
),
child: const Text('+91'),
),
MySpacing.width(12),
Expanded(
child: TextFormField(
controller: phoneController,
validator: (value) {
final v = value?.trim() ?? '';
if (v.isEmpty) return 'Phone Number is required';
if (v.length != 10)
return 'Phone Number must be exactly 10 digits';
if (!RegExp(r'^\d{10}$').hasMatch(v)) {
return 'Enter a valid 10-digit number';
}
return null;
},
keyboardType: TextInputType.number,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
LengthLimitingTextInputFormatter(10),
],
decoration: _inputDecoration('e.g., 9876543210').copyWith(
suffixIcon: IconButton(
icon: const Icon(Icons.contacts),
onPressed: () => _controller.pickContact(context),
),
),
),
),
],
),
],
);
}
// Actions
Future<void> _pickJoiningDate(BuildContext context) async {
final picked = await showDatePicker(
context: context,
initialDate: _controller.joiningDate ?? DateTime.now(),
firstDate: DateTime(2000),
lastDate: DateTime(2100),
);
if (picked != null) {
_controller.setJoiningDate(picked);
_joiningDateController.text = DateFormat('dd MMM yyyy').format(picked);
_controller.update();
}
}
Future<void> _handleSubmit() async {
final isValid =
_controller.basicValidator.formKey.currentState?.validate() ?? false;
if (!isValid ||
_controller.joiningDate == null ||
_controller.selectedGender == null ||
_controller.selectedRoleId == null ||
_organizationController.currentSelection.isEmpty ||
_organizationController.currentSelection == 'All Organizations') {
showAppSnackbar(
title: 'Missing Fields',
message: 'Please complete all required fields.',
type: SnackbarType.warning,
);
return;
}
final result = await _controller.createOrUpdateEmployee(
email: _controller.basicValidator.getController('email')?.text.trim(),
hasApplicationAccess: _hasApplicationAccess,
);
if (result != null && result['success'] == true) {
final employeeController = Get.find<EmployeesScreenController>();
final projectId = employeeController.selectedProjectId;
if (projectId == null) {
await employeeController.fetchAllEmployees();
} else {
await employeeController.fetchEmployeesByProject(projectId);
}
employeeController.update(['employee_screen_controller']);
if (mounted) Navigator.pop(context, result['data']);
}
}
void _showOrganizationPopup(BuildContext context) async {
final orgs = _organizationController.organizations;
if (orgs.isEmpty) {
showAppSnackbar(
title: 'No Organizations',
message: 'No organizations available to select.',
type: SnackbarType.warning,
);
return;
}
final selected = await showMenu<String>(
context: context,
position: _popupMenuPosition(context),
items: orgs
.map(
(org) => PopupMenuItem<String>(
value: org.id,
child: Text(org.name),
),
)
.toList(),
);
if (selected != null && selected.trim().isNotEmpty) {
final chosen = orgs.firstWhere((e) => e.id == selected);
_organizationController.selectOrganization(chosen);
_controller.selectedOrganizationId = chosen.id;
_orgFieldController.text = chosen.name;
_controller.update();
}
}
void _showGenderPopup(BuildContext context) async { void _showGenderPopup(BuildContext context) async {
final selected = await showMenu<Gender>( final selected = await showMenu<Gender>(
context: context, context: context,
position: _popupMenuPosition(context), position: _popupMenuPosition(context),
items: Gender.values.map((gender) { items: Gender.values
return PopupMenuItem<Gender>( .map(
value: gender, (gender) => PopupMenuItem<Gender>(
child: Text(gender.name.capitalizeFirst!), value: gender,
); child: Text(gender.name.capitalizeFirst!),
}).toList(), ),
)
.toList(),
); );
if (selected != null) { if (selected != null) {
_controller.onGenderSelected(selected); _controller.onGenderSelected(selected);
_genderController.text = selected.name.capitalizeFirst ?? '';
_controller.update(); _controller.update();
} }
} }
@ -372,16 +573,22 @@ class _AddEmployeeBottomSheetState extends State<AddEmployeeBottomSheet>
final selected = await showMenu<String>( final selected = await showMenu<String>(
context: context, context: context,
position: _popupMenuPosition(context), position: _popupMenuPosition(context),
items: _controller.roles.map((role) { items: _controller.roles
return PopupMenuItem<String>( .map(
value: role['id'], (role) => PopupMenuItem<String>(
child: Text(role['name']), value: role['id'],
); child: Text(role['name']),
}).toList(), ),
)
.toList(),
); );
if (selected != null) { if (selected != null) {
_controller.onRoleSelected(selected); _controller.onRoleSelected(selected);
final roleName = _controller.roles
.firstWhereOrNull((r) => r['id'] == selected)?['name'] ??
'';
_roleController.text = roleName;
_controller.update(); _controller.update();
} }
} }