feat: refactor AddEmployeeBottomSheet and AssignProjectBottomSheet for improved UI and functionality

This commit is contained in:
Vaibhav Surve 2025-07-31 18:17:48 +05:30
parent 797df80890
commit 6d29d444fa
2 changed files with 386 additions and 573 deletions

View File

@ -1,13 +1,13 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:marco/controller/dashboard/add_employee_controller.dart';
import 'package:marco/controller/dashboard/employees_screen_controller.dart';
import 'package:marco/helpers/utils/mixins/ui_mixin.dart';
import 'package:marco/helpers/widgets/my_spacing.dart';
import 'package:marco/helpers/widgets/my_text.dart';
import 'package:marco/helpers/widgets/my_text_style.dart';
import 'package:marco/helpers/utils/base_bottom_sheet.dart';
class AddEmployeeBottomSheet extends StatefulWidget {
@override
@ -18,69 +18,110 @@ class _AddEmployeeBottomSheetState extends State<AddEmployeeBottomSheet>
with UIMixin {
final AddEmployeeController _controller = Get.put(AddEmployeeController());
late TextEditingController genderController;
late TextEditingController roleController;
@override
void initState() {
super.initState();
genderController = TextEditingController();
roleController = TextEditingController();
}
RelativeRect _popupMenuPosition(BuildContext context) {
final RenderBox overlay =
Overlay.of(context).context.findRenderObject() as RenderBox;
return RelativeRect.fromLTRB(100, 300, overlay.size.width - 100, 0);
}
void _showGenderPopup(BuildContext context) async {
final selected = await showMenu<Gender>(
context: context,
position: _popupMenuPosition(context),
items: Gender.values.map((gender) {
return PopupMenuItem<Gender>(
value: gender,
child: Text(gender.name.capitalizeFirst!),
Widget build(BuildContext context) {
return GetBuilder<AddEmployeeController>(
init: _controller,
builder: (_) {
return BaseBottomSheet(
title: "Add Employee",
onCancel: () => Navigator.pop(context),
onSubmit: _handleSubmit,
child: Form(
key: _controller.basicValidator.formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_sectionLabel("Personal Info"),
MySpacing.height(16),
_inputWithIcon(
label: "First Name",
hint: "e.g., John",
icon: Icons.person,
controller:
_controller.basicValidator.getController('first_name')!,
validator:
_controller.basicValidator.getValidation('first_name'),
),
MySpacing.height(16),
_inputWithIcon(
label: "Last Name",
hint: "e.g., Doe",
icon: Icons.person_outline,
controller:
_controller.basicValidator.getController('last_name')!,
validator:
_controller.basicValidator.getValidation('last_name'),
),
MySpacing.height(16),
_sectionLabel("Contact Details"),
MySpacing.height(16),
_buildPhoneInput(context),
MySpacing.height(24),
_sectionLabel("Other Details"),
MySpacing.height(16),
_buildDropdownField(
label: "Gender",
value: _controller.selectedGender?.name.capitalizeFirst ?? '',
hint: "Select Gender",
onTap: () => _showGenderPopup(context),
),
MySpacing.height(16),
_buildDropdownField(
label: "Role",
value: _controller.roles.firstWhereOrNull((role) =>
role['id'] == _controller.selectedRoleId)?['name'] ??
"",
hint: "Select Role",
onTap: () => _showRolePopup(context),
),
],
),
),
);
}).toList(),
},
);
}
if (selected != null) {
_controller.onGenderSelected(selected);
// Submit logic
Future<void> _handleSubmit() async {
final result = await _controller.createEmployees();
if (result != null && result['success'] == true) {
final employeeData = result['data']; // Safe now
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']);
_controller.basicValidator.getController("first_name")?.clear();
_controller.basicValidator.getController("last_name")?.clear();
_controller.basicValidator.getController("phone_number")?.clear();
_controller.selectedGender = null;
_controller.selectedRoleId = null;
_controller.update();
Navigator.pop(context, employeeData);
}
}
void _showRolePopup(BuildContext context) async {
final selected = await showMenu<String>(
context: context,
position: _popupMenuPosition(context),
items: _controller.roles.map((role) {
return PopupMenuItem<String>(
value: role['id'],
child: Text(role['name']),
);
}).toList(),
);
if (selected != null) {
_controller.onRoleSelected(selected);
_controller.update();
}
}
Widget _sectionLabel(String title) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MyText.labelLarge(title, fontWeight: 600),
MySpacing.height(4),
Divider(thickness: 1, color: Colors.grey.shade200),
],
);
}
// Section label widget
Widget _sectionLabel(String title) => Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MyText.labelLarge(title, fontWeight: 600),
MySpacing.height(4),
Divider(thickness: 1, color: Colors.grey.shade200),
],
);
// Input field with icon
Widget _inputWithIcon({
required String label,
required String hint,
@ -104,6 +145,124 @@ class _AddEmployeeBottomSheetState extends State<AddEmployeeBottomSheet>
);
}
// Phone input with country code selector
Widget _buildPhoneInput(BuildContext context) {
return 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: PopupMenuButton<Map<String, String>>(
onSelected: (country) {
_controller.selectedCountryCode = country['code']!;
_controller.update();
},
itemBuilder: (context) => [
PopupMenuItem(
enabled: false,
padding: EdgeInsets.zero,
child: SizedBox(
height: 200,
width: 100,
child: ListView(
children: _controller.countries.map((country) {
return ListTile(
dense: true,
title: Text("${country['name']} (${country['code']})"),
onTap: () => Navigator.pop(context, country),
);
}).toList(),
),
),
),
],
child: Row(
children: [
Text(_controller.selectedCountryCode),
const Icon(Icons.arrow_drop_down),
],
),
),
),
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";
}
final digitsOnly = value.trim();
final minLength = _controller
.minDigitsPerCountry[_controller.selectedCountryCode] ??
7;
final maxLength = _controller
.maxDigitsPerCountry[_controller.selectedCountryCode] ??
15;
if (!RegExp(r'^[0-9]+$').hasMatch(digitsOnly)) {
return "Only digits allowed";
}
if (digitsOnly.length < minLength ||
digitsOnly.length > maxLength) {
return "Between $minLength$maxLength digits";
}
return null;
},
keyboardType: TextInputType.phone,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
LengthLimitingTextInputFormatter(15),
],
decoration: _inputDecoration("e.g., 9876543210").copyWith(
suffixIcon: IconButton(
icon: const Icon(Icons.contacts),
onPressed: () => _controller.pickContact(context),
),
),
),
),
],
);
}
// Gender/Role field (read-only dropdown)
Widget _buildDropdownField({
required String label,
required String value,
required String hint,
required VoidCallback onTap,
}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MyText.labelMedium(label),
MySpacing.height(8),
GestureDetector(
onTap: onTap,
child: AbsorbPointer(
child: TextFormField(
readOnly: true,
controller: TextEditingController(text: value),
decoration: _inputDecoration(hint).copyWith(
suffixIcon: const Icon(Icons.expand_more),
),
),
),
),
],
);
}
// Common input decoration
InputDecoration _inputDecoration(String hint) {
return InputDecoration(
hintText: hint,
@ -120,311 +279,53 @@ class _AddEmployeeBottomSheetState extends State<AddEmployeeBottomSheet>
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.blueAccent, width: 1.5),
borderSide: const BorderSide(color: Colors.blueAccent, width: 1.5),
),
contentPadding: MySpacing.all(16),
);
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return GetBuilder<AddEmployeeController>(
init: _controller,
builder: (_) {
return SingleChildScrollView(
padding: MediaQuery.of(context).viewInsets,
child: Container(
decoration: BoxDecoration(
color: theme.cardColor,
borderRadius:
const BorderRadius.vertical(top: Radius.circular(24)),
boxShadow: const [
BoxShadow(
color: Colors.black12,
blurRadius: 12,
offset: Offset(0, -2))
],
),
child: Padding(
padding: const EdgeInsets.fromLTRB(20, 16, 20, 32),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Drag Handle
Container(
width: 40,
height: 5,
decoration: BoxDecoration(
color: Colors.grey.shade300,
borderRadius: BorderRadius.circular(10),
),
),
MySpacing.height(12),
Text("Add Employee",
style: MyTextStyle.titleLarge(fontWeight: 700)),
MySpacing.height(24),
Form(
key: _controller.basicValidator.formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_sectionLabel("Personal Info"),
MySpacing.height(16),
_inputWithIcon(
label: "First Name",
hint: "e.g., John",
icon: Icons.person,
controller: _controller.basicValidator
.getController('first_name')!,
validator: _controller.basicValidator
.getValidation('first_name'),
),
MySpacing.height(16),
_inputWithIcon(
label: "Last Name",
hint: "e.g., Doe",
icon: Icons.person_outline,
controller: _controller.basicValidator
.getController('last_name')!,
validator: _controller.basicValidator
.getValidation('last_name'),
),
MySpacing.height(16),
_sectionLabel("Contact Details"),
MySpacing.height(16),
MyText.labelMedium("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: PopupMenuButton<Map<String, String>>(
onSelected: (country) {
_controller.selectedCountryCode =
country['code']!;
_controller.update();
},
itemBuilder: (context) => [
PopupMenuItem(
enabled: false,
padding: EdgeInsets.zero,
child: SizedBox(
height: 200,
width: 100,
child: ListView(
children: _controller.countries
.map((country) {
return ListTile(
dense: true,
title: Text(
"${country['name']} (${country['code']})"),
onTap: () =>
Navigator.pop(context, country),
);
}).toList(),
),
),
),
],
child: Row(
children: [
Text(_controller.selectedCountryCode),
const Icon(Icons.arrow_drop_down),
],
),
),
),
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";
}
final digitsOnly = value.trim();
final minLength = _controller
.minDigitsPerCountry[
_controller.selectedCountryCode] ??
7;
final maxLength = _controller
.maxDigitsPerCountry[
_controller.selectedCountryCode] ??
15;
if (!RegExp(r'^[0-9]+$')
.hasMatch(digitsOnly)) {
return "Only digits allowed";
}
if (digitsOnly.length < minLength ||
digitsOnly.length > maxLength) {
return "Between $minLength$maxLength digits";
}
return null;
},
keyboardType: TextInputType.phone,
inputFormatters: [
// Allow only digits
FilteringTextInputFormatter.digitsOnly,
// Limit to 10 digits
LengthLimitingTextInputFormatter(10),
],
decoration: _inputDecoration("e.g., 9876543210")
.copyWith(
suffixIcon: IconButton(
icon: const Icon(Icons.contacts),
onPressed: () =>
_controller.pickContact(context),
),
),
),
),
],
),
MySpacing.height(24),
_sectionLabel("Other Details"),
MySpacing.height(16),
MyText.labelMedium("Gender"),
MySpacing.height(8),
GestureDetector(
onTap: () => _showGenderPopup(context),
child: AbsorbPointer(
child: TextFormField(
readOnly: true,
controller: TextEditingController(
text: _controller
.selectedGender?.name.capitalizeFirst,
),
decoration:
_inputDecoration("Select Gender").copyWith(
suffixIcon: const Icon(Icons.expand_more),
),
),
),
),
MySpacing.height(16),
MyText.labelMedium("Role"),
MySpacing.height(8),
GestureDetector(
onTap: () => _showRolePopup(context),
child: AbsorbPointer(
child: TextFormField(
readOnly: true,
controller: TextEditingController(
text: _controller.roles.firstWhereOrNull(
(role) =>
role['id'] ==
_controller.selectedRoleId,
)?['name'] ??
"",
),
decoration:
_inputDecoration("Select Role").copyWith(
suffixIcon: const Icon(Icons.expand_more),
),
),
),
),
MySpacing.height(16),
Row(
children: [
Expanded(
child: OutlinedButton.icon(
onPressed: () => Navigator.pop(context),
icon:
const Icon(Icons.close, color: Colors.red),
label: MyText.bodyMedium("Cancel",
color: Colors.red, fontWeight: 600),
style: OutlinedButton.styleFrom(
side: const BorderSide(color: Colors.red),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12)),
padding: const EdgeInsets.symmetric(
horizontal: 20, vertical: 14),
),
),
),
const SizedBox(width: 12),
Expanded(
child: ElevatedButton.icon(
onPressed: () async {
if (_controller.basicValidator
.validateForm()) {
final result =
await _controller.createEmployees();
if (result != null &&
result['success'] == true) {
final employeeData = result['data'];
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']);
_controller.basicValidator
.getController("first_name")
?.clear();
_controller.basicValidator
.getController("last_name")
?.clear();
_controller.basicValidator
.getController("phone_number")
?.clear();
_controller.selectedGender = null;
_controller.selectedRoleId = null;
_controller.update();
Navigator.pop(context, employeeData);
}
}
},
icon: const Icon(Icons.check_circle_outline,
color: Colors.white),
label: MyText.bodyMedium("Save",
color: Colors.white, fontWeight: 600),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.indigo,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12)),
padding: const EdgeInsets.symmetric(
horizontal: 28, vertical: 14),
),
),
),
],
),
],
),
),
],
),
),
),
// Gender popup menu
void _showGenderPopup(BuildContext context) async {
final selected = await showMenu<Gender>(
context: context,
position: _popupMenuPosition(context),
items: Gender.values.map((gender) {
return PopupMenuItem<Gender>(
value: gender,
child: Text(gender.name.capitalizeFirst!),
);
},
}).toList(),
);
if (selected != null) {
_controller.onGenderSelected(selected);
_controller.update();
}
}
// Role popup menu
void _showRolePopup(BuildContext context) async {
final selected = await showMenu<String>(
context: context,
position: _popupMenuPosition(context),
items: _controller.roles.map((role) {
return PopupMenuItem<String>(
value: role['id'],
child: Text(role['name']),
);
}).toList(),
);
if (selected != null) {
_controller.onRoleSelected(selected);
_controller.update();
}
}
RelativeRect _popupMenuPosition(BuildContext context) {
final RenderBox overlay =
Overlay.of(context).context.findRenderObject() as RenderBox;
return RelativeRect.fromLTRB(100, 300, overlay.size.width - 100, 0);
}
}

View File

@ -2,9 +2,10 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:marco/helpers/widgets/my_spacing.dart';
import 'package:marco/helpers/widgets/my_text.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart';
import 'package:marco/controller/employee/assign_projects_controller.dart';
import 'package:marco/model/global_project_model.dart';
import 'package:marco/helpers/widgets/my_snackbar.dart';
import 'package:marco/helpers/utils/base_bottom_sheet.dart';
class AssignProjectBottomSheet extends StatefulWidget {
final String employeeId;
@ -23,6 +24,7 @@ class AssignProjectBottomSheet extends StatefulWidget {
class _AssignProjectBottomSheetState extends State<AssignProjectBottomSheet> {
late final AssignProjectController assignController;
final ScrollController _scrollController = ScrollController();
@override
void initState() {
@ -38,229 +40,139 @@ class _AssignProjectBottomSheetState extends State<AssignProjectBottomSheet> {
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return GetBuilder<AssignProjectController>(
tag: '${widget.employeeId}_${widget.jobRoleId}',
builder: (_) {
return BaseBottomSheet(
title: "Assign to Project",
onCancel: () => Navigator.pop(context),
onSubmit: _handleAssign,
submitText: "Assign",
child: Obx(() {
if (assignController.isLoading.value) {
return const Center(child: CircularProgressIndicator());
}
return SafeArea(
top: false,
child: DraggableScrollableSheet(
expand: false,
maxChildSize: 0.9,
minChildSize: 0.4,
initialChildSize: 0.7,
builder: (_, scrollController) {
return Container(
decoration: BoxDecoration(
color: theme.cardColor,
borderRadius:
const BorderRadius.vertical(top: Radius.circular(24)),
boxShadow: const [
BoxShadow(
color: Colors.black12,
blurRadius: 12,
offset: Offset(0, -2),
final projects = assignController.allProjects;
if (projects.isEmpty) {
return const Center(child: Text('No projects available.'));
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MyText.bodySmall(
'Select the projects to assign this employee.',
color: Colors.grey[600],
),
MySpacing.height(8),
// Select All
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Projects (${projects.length})',
style: const TextStyle(
fontWeight: FontWeight.w600,
fontSize: 16,
),
),
TextButton(
onPressed: () {
assignController.toggleSelectAll();
},
child: Obx(() {
return Text(
assignController.areAllSelected()
? 'Deselect All'
: 'Select All',
style: const TextStyle(
color: Colors.blueAccent,
fontWeight: FontWeight.w600,
),
);
}),
),
],
),
// List of Projects
SizedBox(
height: 300,
child: ListView.builder(
controller: _scrollController,
itemCount: projects.length,
itemBuilder: (context, index) {
final GlobalProjectModel project = projects[index];
return Obx(() {
final bool isSelected =
assignController.isProjectSelected(
project.id.toString(),
);
return Theme(
data: Theme.of(context).copyWith(
checkboxTheme: CheckboxThemeData(
fillColor: WidgetStateProperty.resolveWith<Color>(
(states) => states.contains(WidgetState.selected)
? Colors.blueAccent
: Colors.white,
),
side: const BorderSide(
color: Colors.black,
width: 2,
),
checkColor:
WidgetStateProperty.all(Colors.white),
),
),
child: CheckboxListTile(
dense: true,
value: isSelected,
title: Text(
project.name,
style: const TextStyle(
fontWeight: FontWeight.w500,
fontSize: 14,
),
),
onChanged: (checked) {
assignController.toggleProjectSelection(
project.id.toString(),
checked ?? false,
);
},
activeColor: Colors.blueAccent,
controlAffinity: ListTileControlAffinity.leading,
contentPadding: EdgeInsets.zero,
visualDensity: const VisualDensity(
horizontal: -4,
vertical: -4,
),
),
);
});
},
),
),
],
),
padding: MySpacing.all(16),
child: Obx(() {
if (assignController.isLoading.value) {
return const Center(child: CircularProgressIndicator());
}
final projects = assignController.allProjects;
if (projects.isEmpty) {
return const Center(child: Text('No projects available.'));
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Drag Handle
Center(
child: Container(
width: 40,
height: 5,
decoration: BoxDecoration(
color: Colors.grey.shade300,
borderRadius: BorderRadius.circular(10),
),
),
),
MySpacing.height(12),
// Header
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
MyText.titleMedium('Assign to Project', fontWeight: 700),
],
),
MySpacing.height(4),
// Sub Info
MyText.bodySmall(
'Select the projects to assign this employee.',
color: Colors.grey[600],
),
MySpacing.height(8),
// Select All Toggle
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Projects (${projects.length})',
style: const TextStyle(
fontWeight: FontWeight.w600,
fontSize: 16,
),
),
TextButton(
onPressed: () {
assignController.toggleSelectAll();
},
child: Obx(() {
return Text(
assignController.areAllSelected()
? 'Deselect All'
: 'Select All',
style: const TextStyle(
color: Colors.blueAccent,
fontWeight: FontWeight.w600,
),
);
}),
),
],
),
// Project List
Expanded(
child: ListView.builder(
controller: scrollController,
itemCount: projects.length,
padding: EdgeInsets.zero,
itemBuilder: (context, index) {
final GlobalProjectModel project = projects[index];
return Obx(() {
final bool isSelected =
assignController.isProjectSelected(
project.id.toString(),
);
return Theme(
data: Theme.of(context).copyWith(
checkboxTheme: CheckboxThemeData(
fillColor:
WidgetStateProperty.resolveWith<Color>(
(states) {
if (states.contains(WidgetState.selected)) {
return Colors.blueAccent;
}
return Colors.white;
},
),
side: const BorderSide(
color: Colors.black,
width: 2,
),
checkColor:
WidgetStateProperty.all(Colors.white),
),
),
child: CheckboxListTile(
dense: true,
value: isSelected,
title: Text(
project.name,
style: const TextStyle(
fontWeight: FontWeight.w500,
fontSize: 14,
),
),
onChanged: (checked) {
assignController.toggleProjectSelection(
project.id.toString(),
checked ?? false,
);
},
activeColor: Colors.blueAccent,
controlAffinity: ListTileControlAffinity.leading,
contentPadding: EdgeInsets.zero,
visualDensity: const VisualDensity(
horizontal: -4,
vertical: -4,
),
),
);
});
},
),
),
MySpacing.height(16),
// Cancel & Save Buttons
Row(
children: [
Expanded(
child: OutlinedButton.icon(
onPressed: () => Navigator.pop(context),
icon: const Icon(Icons.close, color: Colors.red),
label: MyText.bodyMedium("Cancel",
color: Colors.red, fontWeight: 600),
style: OutlinedButton.styleFrom(
side: const BorderSide(color: Colors.red),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
padding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 7,
),
),
),
),
const SizedBox(width: 12),
Expanded(
child: ElevatedButton.icon(
onPressed: () async {
if (assignController.selectedProjects.isEmpty) {
showAppSnackbar(
title: "Error",
message: "Please select at least one project.",
type: SnackbarType.error,
);
return;
}
await _assignProjects();
},
icon: const Icon(Icons.check_circle_outline,
color: Colors.white),
label: MyText.bodyMedium("Assign",
color: Colors.white, fontWeight: 600),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.indigo,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
padding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 7,
),
),
),
),
],
),
],
);
}),
);
},
),
);
}),
);
},
);
}
Future<void> _assignProjects() async {
Future<void> _handleAssign() async {
if (assignController.selectedProjects.isEmpty) {
showAppSnackbar(
title: "Error",
message: "Please select at least one project.",
type: SnackbarType.error,
);
return;
}
final success = await assignController.assignProjectsToEmployee();
if (success) {
Get.back();