import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:marco/helpers/theme/app_theme.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/model/employees/add_employee_bottom_sheet.dart'; import 'package:marco/controller/dashboard/employees_screen_controller.dart'; import 'package:marco/helpers/widgets/avatar.dart'; import 'package:marco/controller/project_controller.dart'; import 'package:marco/helpers/widgets/my_custom_skeleton.dart'; import 'package:marco/view/employees/employee_detail_screen.dart'; import 'package:marco/model/employee_model.dart'; import 'package:marco/helpers/utils/launcher_utils.dart'; import 'package:marco/view/employees/assign_employee_bottom_sheet.dart'; class EmployeesScreen extends StatefulWidget { const EmployeesScreen({super.key}); @override State createState() => _EmployeesScreenState(); } class _EmployeesScreenState extends State with UIMixin { final EmployeesScreenController _employeeController = Get.put(EmployeesScreenController()); final TextEditingController _searchController = TextEditingController(); final RxList _filteredEmployees = [].obs; @override void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) { _initEmployees(); _searchController.addListener(() { _filterEmployees(_searchController.text); }); }); } Future _initEmployees() async { final selectedProjectId = Get.find().selectedProject?.id; if (selectedProjectId != null) { _employeeController.selectedProjectId = selectedProjectId; await _employeeController.fetchEmployeesByProject(selectedProjectId); } else if (_employeeController.isAllEmployeeSelected.value) { _employeeController.selectedProjectId = null; await _employeeController.fetchAllEmployees(); } else { _employeeController.clearEmployees(); } _filterEmployees(_searchController.text); } Future _refreshEmployees() async { try { final selectedProjectId = Get.find().selectedProject?.id; final isAllSelected = _employeeController.isAllEmployeeSelected.value; if (isAllSelected) { _employeeController.selectedProjectId = null; await _employeeController.fetchAllEmployees(); } else if (selectedProjectId != null) { _employeeController.selectedProjectId = selectedProjectId; await _employeeController.fetchEmployeesByProject(selectedProjectId); } else { _employeeController.clearEmployees(); } _filterEmployees(_searchController.text); _employeeController.update(['employee_screen_controller']); } catch (e, stackTrace) { debugPrint('Error refreshing employee data: $e'); debugPrintStack(stackTrace: stackTrace); } } void _filterEmployees(String query) { final employees = _employeeController.employees; if (query.isEmpty) { _filteredEmployees.assignAll(employees); return; } final lowerQuery = 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)), ); } Future _onAddNewEmployee() async { final result = await showModalBottomSheet>( context: context, isScrollControlled: true, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(16))), backgroundColor: Colors.transparent, builder: (context) => AddEmployeeBottomSheet(), ); if (result == null || result['success'] != true) return; final employeeData = result['data']; final employeeId = employeeData['id'] as String; final jobRoleId = employeeData['jobRoleId'] as String?; await showModalBottomSheet( context: context, isScrollControlled: true, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(24))), backgroundColor: Colors.transparent, builder: (context) => AssignProjectBottomSheet( employeeId: employeeId, jobRoleId: jobRoleId ?? '', ), ); await _refreshEmployees(); } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.white, appBar: _buildAppBar(), floatingActionButton: _buildFloatingActionButton(), body: SafeArea( child: GetBuilder( init: _employeeController, tag: 'employee_screen_controller', builder: (controller) { _filterEmployees(_searchController.text); return SingleChildScrollView( padding: const EdgeInsets.only(bottom: 40), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ MySpacing.height(flexSpacing), _buildSearchAndActionRow(), MySpacing.height(flexSpacing), Padding( padding: MySpacing.x(flexSpacing), child: _buildEmployeeList(), ), ], ), ); }, ), ), ); } PreferredSizeWidget _buildAppBar() { return PreferredSize( preferredSize: const Size.fromHeight(72), child: AppBar( backgroundColor: const Color(0xFFF5F5F5), elevation: 0.5, automaticallyImplyLeading: false, titleSpacing: 0, 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), 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, ), MySpacing.height(2), GetBuilder( builder: (projectController) { final projectName = projectController.selectedProject?.name ?? 'Select Project'; return Row( children: [ const Icon(Icons.work_outline, size: 14, color: Colors.grey), MySpacing.width(4), Expanded( child: MyText.bodySmall( projectName, fontWeight: 600, overflow: TextOverflow.ellipsis, color: Colors.grey[700], ), ), ], ); }, ), ], ), ), ], ), ), ), ); } Widget _buildFloatingActionButton() { return InkWell( onTap: _onAddNewEmployee, borderRadius: BorderRadius.circular(28), child: Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), decoration: BoxDecoration( color: Colors.red, borderRadius: BorderRadius.circular(28), boxShadow: const [ BoxShadow( color: Colors.black26, blurRadius: 6, offset: Offset(0, 3), ), ], ), child: const Row( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.add, color: Colors.white), SizedBox(width: 8), Text('Add New Employee', style: TextStyle(color: Colors.white)), ], ), ), ); } Widget _buildSearchAndActionRow() { return Padding( padding: MySpacing.x(flexSpacing), child: Row( children: [ Expanded(child: _buildSearchField()), const SizedBox(width: 8), _buildRefreshButton(), const SizedBox(width: 4), _buildPopupMenu(), ], ), ); } Widget _buildSearchField() { return SizedBox( height: 36, child: TextField( controller: _searchController, style: const TextStyle(fontSize: 13, height: 1.2), decoration: InputDecoration( isDense: true, 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), hintText: 'Search contacts...', hintStyle: const TextStyle(fontSize: 13, color: Colors.grey), filled: true, fillColor: Colors.white, border: OutlineInputBorder( borderRadius: BorderRadius.circular(8), borderSide: BorderSide(color: Colors.grey.shade300, width: 1), ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(8), borderSide: BorderSide(color: Colors.grey.shade300, width: 1), ), suffixIcon: _searchController.text.isNotEmpty ? GestureDetector( onTap: () { _searchController.clear(); _filterEmployees(''); setState(() {}); }, child: const Icon(Icons.close, size: 18, color: Colors.grey), ) : null, ), onChanged: (_) => setState(() {}), ), ); } Widget _buildRefreshButton() { return Tooltip( message: 'Refresh Data', child: InkWell( borderRadius: BorderRadius.circular(24), onTap: _refreshEmployees, child: const Padding( padding: EdgeInsets.all(10), child: Icon(Icons.refresh, color: Colors.green, size: 28), ), ), ); } Widget _buildPopupMenu() { return PopupMenuButton( icon: Stack( 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(); }), ], ), 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().selectedProject?.id; if (selectedProjectId != null) { _employeeController.selectedProjectId = selectedProjectId; await _employeeController .fetchEmployeesByProject(selectedProjectId); } else { _employeeController.clearEmployees(); } } _filterEmployees(_searchController.text); _employeeController.update(['employee_screen_controller']); } }, itemBuilder: (context) => [ PopupMenuItem( value: 'all_employees', child: Obx( () => Row( children: [ Checkbox( value: _employeeController.isAllEmployeeSelected.value, onChanged: (bool? value) => Navigator.pop(context, 'all_employees'), checkColor: Colors.white, activeColor: Colors.red, side: const BorderSide(color: Colors.black, width: 1.5), fillColor: MaterialStateProperty.resolveWith((states) { if (states.contains(MaterialState.selected)) { return Colors.red; } return Colors.white; }), ), const Text('All Employees'), ], ), ), ), ], ); } Widget _buildEmployeeList() { return Obx(() { final isLoading = _employeeController.isLoading.value; final employees = _filteredEmployees; // Show skeleton loader while data is being fetched if (isLoading) { return ListView.separated( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), itemCount: 8, // number of skeleton items separatorBuilder: (_, __) => MySpacing.height(12), itemBuilder: (_, __) => SkeletonLoaders.employeeSkeletonCard(), ); } // Show empty state when no employees are found 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], ), ), ); } // 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 : ''; 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, ), ], ), ), ], ), ), const Icon(Icons.arrow_forward_ios, color: Colors.grey, size: 16), ], ), ), ); }, ); }); } }