feat: Refactor EmployeesScreen for improved readability and structure

This commit is contained in:
Vaibhav Surve 2025-08-02 15:34:38 +05:30
parent 0f0eb51c15
commit 70443d8e24

View File

@ -22,8 +22,7 @@ class EmployeesScreen extends StatefulWidget {
}
class _EmployeesScreenState extends State<EmployeesScreen> with UIMixin {
final EmployeesScreenController _employeeController =
Get.put(EmployeesScreenController());
final EmployeesScreenController _employeeController = Get.put(EmployeesScreenController());
final TextEditingController _searchController = TextEditingController();
final RxList<EmployeeModel> _filteredEmployees = <EmployeeModel>[].obs;
@ -32,39 +31,37 @@ class _EmployeesScreenState extends State<EmployeesScreen> with UIMixin {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
_initEmployees();
_searchController.addListener(() {
_filterEmployees(_searchController.text);
});
_searchController.addListener(() => _filterEmployees(_searchController.text));
});
}
Future<void> _initEmployees() async {
final selectedProjectId = Get.find<ProjectController>().selectedProject?.id;
final projectId = Get.find<ProjectController>().selectedProject?.id;
if (selectedProjectId != null) {
_employeeController.selectedProjectId = selectedProjectId;
await _employeeController.fetchEmployeesByProject(selectedProjectId);
} else if (_employeeController.isAllEmployeeSelected.value) {
if (_employeeController.isAllEmployeeSelected.value) {
_employeeController.selectedProjectId = null;
await _employeeController.fetchAllEmployees();
} else if (projectId != null) {
_employeeController.selectedProjectId = projectId;
await _employeeController.fetchEmployeesByProject(projectId);
} else {
_employeeController.clearEmployees();
}
_filterEmployees(_searchController.text);
}
Future<void> _refreshEmployees() async {
try {
final selectedProjectId =
Get.find<ProjectController>().selectedProject?.id;
final isAllSelected = _employeeController.isAllEmployeeSelected.value;
final projectId = Get.find<ProjectController>().selectedProject?.id;
final allSelected = _employeeController.isAllEmployeeSelected.value;
if (isAllSelected) {
_employeeController.selectedProjectId = null;
_employeeController.selectedProjectId = allSelected ? null : projectId;
if (allSelected) {
await _employeeController.fetchAllEmployees();
} else if (selectedProjectId != null) {
_employeeController.selectedProjectId = selectedProjectId;
await _employeeController.fetchEmployeesByProject(selectedProjectId);
} else if (projectId != null) {
await _employeeController.fetchEmployeesByProject(projectId);
} else {
_employeeController.clearEmployees();
}
@ -79,17 +76,20 @@ class _EmployeesScreenState extends State<EmployeesScreen> with UIMixin {
void _filterEmployees(String query) {
final employees = _employeeController.employees;
if (query.isEmpty) {
_filteredEmployees.assignAll(employees);
return;
}
final lowerQuery = query.toLowerCase();
final q = query.toLowerCase();
_filteredEmployees.assignAll(
employees.where((e) =>
e.name.toLowerCase().contains(lowerQuery) ||
e.email.toLowerCase().contains(lowerQuery) ||
e.phoneNumber.toLowerCase().contains(lowerQuery) ||
e.jobRole.toLowerCase().contains(lowerQuery)),
e.name.toLowerCase().contains(q) ||
e.email.toLowerCase().contains(q) ||
e.phoneNumber.toLowerCase().contains(q) ||
e.jobRole.toLowerCase().contains(q),
),
);
}
@ -98,7 +98,8 @@ class _EmployeesScreenState extends State<EmployeesScreen> with UIMixin {
context: context,
isScrollControlled: true,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(16))),
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
),
backgroundColor: Colors.transparent,
builder: (context) => AddEmployeeBottomSheet(),
);
@ -113,7 +114,8 @@ class _EmployeesScreenState extends State<EmployeesScreen> with UIMixin {
context: context,
isScrollControlled: true,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(24))),
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
),
backgroundColor: Colors.transparent,
builder: (context) => AssignProjectBottomSheet(
employeeId: employeeId,
@ -134,7 +136,7 @@ class _EmployeesScreenState extends State<EmployeesScreen> with UIMixin {
child: GetBuilder<EmployeesScreenController>(
init: _employeeController,
tag: 'employee_screen_controller',
builder: (controller) {
builder: (_) {
_filterEmployees(_searchController.text);
return SingleChildScrollView(
padding: const EdgeInsets.only(bottom: 40),
@ -168,34 +170,24 @@ class _EmployeesScreenState extends State<EmployeesScreen> with UIMixin {
title: Padding(
padding: MySpacing.xy(16, 0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
IconButton(
icon: const Icon(Icons.arrow_back_ios_new,
color: Colors.black, size: 20),
icon: const Icon(Icons.arrow_back_ios_new, color: Colors.black, size: 20),
onPressed: () => Get.offNamed('/dashboard'),
),
MySpacing.width(8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
MyText.titleLarge(
'Employees',
fontWeight: 700,
color: Colors.black,
),
MyText.titleLarge('Employees', fontWeight: 700, color: Colors.black),
MySpacing.height(2),
GetBuilder<ProjectController>(
builder: (projectController) {
final projectName =
projectController.selectedProject?.name ??
'Select Project';
final projectName = projectController.selectedProject?.name ?? 'Select Project';
return Row(
children: [
const Icon(Icons.work_outline,
size: 14, color: Colors.grey),
const Icon(Icons.work_outline, size: 14, color: Colors.grey),
MySpacing.width(4),
Expanded(
child: MyText.bodySmall(
@ -228,13 +220,7 @@ class _EmployeesScreenState extends State<EmployeesScreen> with UIMixin {
decoration: BoxDecoration(
color: Colors.red,
borderRadius: BorderRadius.circular(28),
boxShadow: const [
BoxShadow(
color: Colors.black26,
blurRadius: 6,
offset: Offset(0, 3),
),
],
boxShadow: const [BoxShadow(color: Colors.black26, blurRadius: 6, offset: Offset(0, 3))],
),
child: const Row(
mainAxisSize: MainAxisSize.min,
@ -271,11 +257,9 @@ class _EmployeesScreenState extends State<EmployeesScreen> with UIMixin {
style: const TextStyle(fontSize: 13, height: 1.2),
decoration: InputDecoration(
isDense: true,
contentPadding:
const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
contentPadding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
prefixIcon: const Icon(Icons.search, size: 18, color: Colors.grey),
prefixIconConstraints:
const BoxConstraints(minWidth: 32, minHeight: 32),
prefixIconConstraints: const BoxConstraints(minWidth: 32, minHeight: 32),
hintText: 'Search contacts...',
hintStyle: const TextStyle(fontSize: 13, color: Colors.grey),
filled: true,
@ -324,46 +308,27 @@ class _EmployeesScreenState extends State<EmployeesScreen> with UIMixin {
clipBehavior: Clip.none,
children: [
const Icon(Icons.tune, color: Colors.black),
Obx(() {
return _employeeController.isAllEmployeeSelected.value
? Positioned(
right: -1,
top: -1,
child: Container(
width: 10,
height: 10,
decoration: const BoxDecoration(
color: Colors.red, shape: BoxShape.circle),
),
)
: const SizedBox.shrink();
}),
Obx(() => _employeeController.isAllEmployeeSelected.value
? Positioned(
right: -1,
top: -1,
child: Container(
width: 10,
height: 10,
decoration: const BoxDecoration(color: Colors.red, shape: BoxShape.circle),
),
)
: const SizedBox.shrink()),
],
),
onSelected: (value) async {
if (value == 'all_employees') {
_employeeController.isAllEmployeeSelected.value =
!_employeeController.isAllEmployeeSelected.value;
if (_employeeController.isAllEmployeeSelected.value) {
_employeeController.selectedProjectId = null;
await _employeeController.fetchAllEmployees();
} else {
final selectedProjectId =
Get.find<ProjectController>().selectedProject?.id;
if (selectedProjectId != null) {
_employeeController.selectedProjectId = selectedProjectId;
await _employeeController
.fetchEmployeesByProject(selectedProjectId);
} else {
_employeeController.clearEmployees();
}
}
_filterEmployees(_searchController.text);
_employeeController.isAllEmployeeSelected.toggle();
await _initEmployees();
_employeeController.update(['employee_screen_controller']);
}
},
itemBuilder: (context) => [
itemBuilder: (_) => [
PopupMenuItem<String>(
value: 'all_employees',
child: Obx(
@ -371,17 +336,12 @@ class _EmployeesScreenState extends State<EmployeesScreen> with UIMixin {
children: [
Checkbox(
value: _employeeController.isAllEmployeeSelected.value,
onChanged: (bool? value) =>
Navigator.pop(context, 'all_employees'),
onChanged: (_) => Navigator.pop(context, 'all_employees'),
checkColor: Colors.white,
activeColor: Colors.red,
side: const BorderSide(color: Colors.black, width: 1.5),
fillColor: MaterialStateProperty.resolveWith<Color>((states) {
if (states.contains(MaterialState.selected)) {
return Colors.red;
}
return Colors.white;
}),
fillColor: MaterialStateProperty.resolveWith<Color>(
(states) => states.contains(MaterialState.selected) ? Colors.red : Colors.white),
),
const Text('All Employees'),
],
@ -394,131 +354,95 @@ class _EmployeesScreenState extends State<EmployeesScreen> with UIMixin {
Widget _buildEmployeeList() {
return Obx(() {
final isLoading = _employeeController.isLoading.value;
final employees = _filteredEmployees;
// Show skeleton loader while data is being fetched
if (isLoading) {
if (_employeeController.isLoading.value) {
return ListView.separated(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: 8, // number of skeleton items
itemCount: 8,
separatorBuilder: (_, __) => MySpacing.height(12),
itemBuilder: (_, __) => SkeletonLoaders.employeeSkeletonCard(),
);
}
// Show empty state when no employees are found
final employees = _filteredEmployees;
if (employees.isEmpty) {
return Padding(
padding: const EdgeInsets.only(top: 60),
child: Center(
child: MyText.bodySmall(
"No Employees Found",
fontWeight: 600,
color: Colors.grey[700],
),
child: MyText.bodySmall("No Employees Found", fontWeight: 600, color: Colors.grey[700]),
),
);
}
// Show the actual employee list
return ListView.separated(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
padding: MySpacing.only(bottom: 80),
itemCount: employees.length,
separatorBuilder: (_, __) => MySpacing.height(12),
itemBuilder: (context, index) {
final employee = employees[index];
final nameParts = employee.name.trim().split(' ');
final firstName = nameParts.first;
final lastName = nameParts.length > 1 ? nameParts.last : '';
itemBuilder: (_, index) {
final e = employees[index];
final names = e.name.trim().split(' ');
final firstName = names.first;
final lastName = names.length > 1 ? names.last : '';
return InkWell(
onTap: () =>
Get.to(() => EmployeeDetailPage(employeeId: employee.id)),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 0),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Avatar(firstName: firstName, lastName: lastName, size: 35),
MySpacing.width(12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MyText.titleSmall(
employee.name,
fontWeight: 600,
overflow: TextOverflow.ellipsis,
),
if (employee.jobRole.isNotEmpty)
MyText.bodySmall(
employee.jobRole,
color: Colors.grey[700],
overflow: TextOverflow.ellipsis,
),
MySpacing.height(8),
if (employee.email.isNotEmpty && employee.email != '-')
GestureDetector(
onTap: () =>
LauncherUtils.launchEmail(employee.email),
onLongPress: () => LauncherUtils.copyToClipboard(
employee.email,
typeLabel: 'Email'),
child: Row(
children: [
const Icon(Icons.email_outlined,
size: 16, color: Colors.indigo),
MySpacing.width(4),
ConstrainedBox(
constraints:
const BoxConstraints(maxWidth: 180),
child: MyText.labelSmall(
employee.email,
overflow: TextOverflow.ellipsis,
color: Colors.indigo,
decoration: TextDecoration.underline,
),
),
],
),
),
if (employee.email.isNotEmpty && employee.email != '-')
MySpacing.height(6),
if (employee.phoneNumber.isNotEmpty)
GestureDetector(
onTap: () =>
LauncherUtils.launchPhone(employee.phoneNumber),
onLongPress: () => LauncherUtils.copyToClipboard(
employee.phoneNumber,
typeLabel: 'Phone'),
child: Row(
children: [
const Icon(Icons.phone_outlined,
size: 16, color: Colors.indigo),
MySpacing.width(4),
MyText.labelSmall(
employee.phoneNumber,
color: Colors.indigo,
decoration: TextDecoration.underline,
),
],
),
),
],
),
onTap: () => Get.to(() => EmployeeDetailPage(employeeId: e.id)),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Avatar(firstName: firstName, lastName: lastName, size: 35),
MySpacing.width(12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MyText.titleSmall(e.name, fontWeight: 600, overflow: TextOverflow.ellipsis),
if (e.jobRole.isNotEmpty)
MyText.bodySmall(e.jobRole, color: Colors.grey[700], overflow: TextOverflow.ellipsis),
MySpacing.height(8),
if (e.email.isNotEmpty && e.email != '-')
_buildLinkRow(icon: Icons.email_outlined, text: e.email, onTap: () => LauncherUtils.launchEmail(e.email), onLongPress: () => LauncherUtils.copyToClipboard(e.email, typeLabel: 'Email')),
if (e.email.isNotEmpty && e.email != '-') MySpacing.height(6),
if (e.phoneNumber.isNotEmpty)
_buildLinkRow(icon: Icons.phone_outlined, text: e.phoneNumber, onTap: () => LauncherUtils.launchPhone(e.phoneNumber), onLongPress: () => LauncherUtils.copyToClipboard(e.phoneNumber, typeLabel: 'Phone')),
],
),
const Icon(Icons.arrow_forward_ios,
color: Colors.grey, size: 16),
],
),
),
const Icon(Icons.arrow_forward_ios, color: Colors.grey, size: 16),
],
),
);
},
);
});
}
Widget _buildLinkRow({
required IconData icon,
required String text,
required VoidCallback onTap,
required VoidCallback onLongPress,
}) {
return GestureDetector(
onTap: onTap,
onLongPress: onLongPress,
child: Row(
children: [
Icon(icon, size: 16, color: Colors.indigo),
MySpacing.width(4),
ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 180),
child: MyText.labelSmall(
text,
overflow: TextOverflow.ellipsis,
color: Colors.indigo,
decoration: TextDecoration.underline,
),
),
],
),
);
}
}