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/employee/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/model/employees/employee_model.dart'; import 'package:marco/helpers/utils/launcher_utils.dart'; import 'package:marco/view/employees/assign_employee_bottom_sheet.dart'; import 'package:marco/controller/permission_controller.dart'; import 'package:marco/helpers/utils/permission_constants.dart'; import 'package:marco/helpers/widgets/my_refresh_indicator.dart'; import 'package:marco/view/employees/employee_profile_screen.dart'; import 'package:marco/controller/tenant/organization_selection_controller.dart'; import 'package:marco/helpers/widgets/tenant/organization_selector.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 PermissionController permissionController = Get.put(PermissionController()); final TextEditingController _searchController = TextEditingController(); final RxList _filteredEmployees = [].obs; final OrganizationController _organizationController = Get.put(OrganizationController()); @override void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) { _initEmployees(); _searchController .addListener(() => _filterEmployees(_searchController.text)); }); } Future _initEmployees() async { final projectId = Get.find().selectedProject?.id; final orgId = _organizationController.selectedOrganization.value?.id; if (projectId != null) { await _organizationController.fetchOrganizations(projectId); } if (_employeeController.isAllEmployeeSelected.value) { _employeeController.selectedProjectId = null; await _employeeController.fetchAllEmployees(organizationId: orgId); } else if (projectId != null) { _employeeController.selectedProjectId = projectId; await _employeeController.fetchEmployeesByProject(projectId, organizationId: orgId); } else { _employeeController.clearEmployees(); } _filterEmployees(_searchController.text); } Future _refreshEmployees() async { try { final projectId = Get.find().selectedProject?.id; final orgId = _organizationController.selectedOrganization.value?.id; final allSelected = _employeeController.isAllEmployeeSelected.value; _employeeController.selectedProjectId = allSelected ? null : projectId; if (allSelected) { await _employeeController.fetchAllEmployees(organizationId: orgId); } else if (projectId != null) { await _employeeController.fetchEmployeesByProject(projectId, organizationId: orgId); } 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; List filtered; if (query.isEmpty) { filtered = List.from(employees); } else { final q = query.toLowerCase(); filtered = employees .where( (e) => e.name.toLowerCase().contains(q) || e.email.toLowerCase().contains(q) || e.phoneNumber.toLowerCase().contains(q) || e.jobRole.toLowerCase().contains(q), ) .toList(); } filtered .sort((a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase())); _filteredEmployees.assignAll(filtered); } 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: (_) { _filterEmployees(_searchController.text); return MyRefreshIndicator( onRefresh: _refreshEmployees, child: SingleChildScrollView( physics: const AlwaysScrollableScrollPhysics(), 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( 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, 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() { if (!permissionController.hasPermission(Permissions.manageEmployees)) { return const SizedBox.shrink(); } 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(15), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Search Field Row Row( children: [ Expanded(child: _buildSearchField()), const SizedBox(width: 8), _buildPopupMenu(), ], ), // Organization Selector Row Row( children: [ Expanded( child: OrganizationSelector( controller: _organizationController, height: 36, onSelectionChanged: (org) async { // Make sure the selectedOrganization is updated immediately _organizationController.selectOrganization(org); final projectId = Get.find().selectedProject?.id; if (_employeeController.isAllEmployeeSelected.value) { await _employeeController.fetchAllEmployees( organizationId: _organizationController .selectedOrganization.value?.id); } else if (projectId != null) { await _employeeController.fetchEmployeesByProject( projectId, organizationId: _organizationController .selectedOrganization.value?.id); } _employeeController.update(['employee_screen_controller']); }, ), ), ], ), MySpacing.height(8), ], ), ); } 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 _buildPopupMenu() { if (!permissionController.hasPermission(Permissions.viewAllEmployees)) { return const SizedBox.shrink(); } return PopupMenuButton( icon: Stack( clipBehavior: Clip.none, children: [ const Icon(Icons.tune, color: Colors.black), 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.toggle(); await _initEmployees(); _employeeController.update(['employee_screen_controller']); } }, itemBuilder: (_) => [ PopupMenuItem( value: 'all_employees', child: Obx( () => Row( children: [ Checkbox( value: _employeeController.isAllEmployeeSelected.value, onChanged: (_) => Navigator.pop(context, 'all_employees'), checkColor: Colors.white, activeColor: Colors.blueAccent, side: const BorderSide(color: Colors.black, width: 1.5), fillColor: MaterialStateProperty.resolveWith( (states) => states.contains(MaterialState.selected) ? Colors.blueAccent : Colors.white), ), const Text('All Employees'), ], ), ), ), ], ); } Widget _buildEmployeeList() { return Obx(() { if (_employeeController.isLoading.value) { return ListView.separated( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), itemCount: 8, separatorBuilder: (_, __) => MySpacing.height(12), itemBuilder: (_, __) => SkeletonLoaders.employeeSkeletonCard(), ); } 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]), ), ); } return ListView.separated( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), padding: MySpacing.only(bottom: 80), itemCount: employees.length, separatorBuilder: (_, __) => MySpacing.height(12), 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(() => EmployeeProfilePage(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), ], ), ); }, ); }); } 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, ), ), ], ), ); } }