diff --git a/lib/helpers/services/api_endpoints.dart b/lib/helpers/services/api_endpoints.dart index e73e150..4795c79 100644 --- a/lib/helpers/services/api_endpoints.dart +++ b/lib/helpers/services/api_endpoints.dart @@ -130,6 +130,12 @@ class ApiEndpoints { static const String getAssignedServices = "/Project/get/assigned/services"; static const String getAdvancePayments = '/Expense/get/transactions'; + // Organization Hierarchy endpoints + static const String getOrganizationHierarchyList = + "/organization/hierarchy/list"; + static const String manageOrganizationHierarchy = + "/organization/hierarchy/manage"; + // Service Project Module API Endpoints static const String getServiceProjectsList = "/serviceproject/list"; static const String getServiceProjectDetail = "/serviceproject/details"; diff --git a/lib/helpers/services/api_service.dart b/lib/helpers/services/api_service.dart index 48e5179..d4b7018 100644 --- a/lib/helpers/services/api_service.dart +++ b/lib/helpers/services/api_service.dart @@ -727,6 +727,60 @@ static Future getServiceProjectDetailApi(String proj } } + /// Fetch hierarchy list for an employee + static Future?> getOrganizationHierarchyList( + String employeeId) async { + if (employeeId.isEmpty) throw ArgumentError('employeeId must not be empty'); + final endpoint = "${ApiEndpoints.getOrganizationHierarchyList}/$employeeId"; + + return _getRequest(endpoint).then( + (res) => res != null + ? _parseResponse(res, label: 'Organization Hierarchy List') + : null, + ); + } + + /// Manage (create/update) organization hierarchy (assign reporters) for an employee + /// payload is a List> with objects like: + /// { "reportToId": "", "isPrimary": true, "isActive": true } + static Future manageOrganizationHierarchy({ + required String employeeId, + required List> payload, + }) async { + if (employeeId.isEmpty) throw ArgumentError('employeeId must not be empty'); + + final endpoint = "${ApiEndpoints.manageOrganizationHierarchy}/$employeeId"; + + logSafe("manageOrganizationHierarchy for $employeeId payload: $payload"); + + try { + final response = await _postRequest(endpoint, payload); + if (response == null) { + logSafe("Manage hierarchy failed: null response", + level: LogLevel.error); + return false; + } + + logSafe("Manage hierarchy response status: ${response.statusCode}"); + logSafe("Manage hierarchy response body: ${response.body}"); + + final json = jsonDecode(response.body); + if (json['success'] == true) { + logSafe("Manage hierarchy succeeded"); + return true; + } + + logSafe("Manage hierarchy failed: ${json['message'] ?? 'Unknown error'}", + level: LogLevel.error); + return false; + } catch (e, stack) { + logSafe("Exception while manageOrganizationHierarchy: $e", + level: LogLevel.error); + logSafe("StackTrace: $stack", level: LogLevel.debug); + return false; + } + } + /// Get Master Currencies static Future getMasterCurrenciesApi() async { const endpoint = ApiEndpoints.getMasterCurrencies; diff --git a/lib/model/employees/employee_details_model.dart b/lib/model/employees/employee_details_model.dart index b5c22f4..e999836 100644 --- a/lib/model/employees/employee_details_model.dart +++ b/lib/model/employees/employee_details_model.dart @@ -22,7 +22,9 @@ class EmployeeDetailsModel { final bool hasApplicationAccess; final String? organizationId; final String? aadharNumber; - final String? panNumber; + final String? panNumber; + + EmployeeDetailsModel({ required this.id, required this.firstName, diff --git a/lib/view/employees/employee_detail_screen.dart b/lib/view/employees/employee_detail_screen.dart index 81b32de..7450384 100644 --- a/lib/view/employees/employee_detail_screen.dart +++ b/lib/view/employees/employee_detail_screen.dart @@ -11,6 +11,8 @@ import 'package:marco/helpers/widgets/my_refresh_indicator.dart'; import 'package:marco/model/employees/add_employee_bottom_sheet.dart'; import 'package:marco/helpers/utils/mixins/ui_mixin.dart'; import 'package:marco/helpers/widgets/my_custom_skeleton.dart'; +import 'package:marco/view/employees/manage_reporting_bottom_sheet.dart'; +import 'package:marco/model/employees/employee_model.dart'; import 'package:marco/view/employees/assign_employee_bottom_sheet.dart'; class EmployeeDetailPage extends StatefulWidget { @@ -269,6 +271,64 @@ class _EmployeeDetailPageState extends State with UIMixin { ), MySpacing.height(16), + _buildSectionCard( + title: 'Manage Reporting', + titleIcon: Icons.people_outline, + children: [ + GestureDetector( + onTap: () async { + await showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (_) => ManageReportingBottomSheet( + initialEmployee: EmployeeModel( + id: employee.id, + employeeId: employee.id.toString(), + firstName: employee.firstName ?? "", + lastName: employee.lastName ?? "", + name: + "${employee.firstName} ${employee.lastName}", + email: employee.email ?? "", + jobRole: employee.jobRole ?? "", + jobRoleID: "0", + designation: employee.jobRole ?? "", + phoneNumber: employee.phoneNumber ?? "", + activity: 0, + action: 0, + ), + hideMainSelector: true, + hideLoggedUserFromSelection: + true, + ), + ); + }, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 12), + child: Row( + children: [ + const Icon(Icons.manage_accounts_outlined, + color: Colors.grey), + const SizedBox(width: 16), + const Expanded( + child: Text( + 'View / Update Reporting Managers', + style: TextStyle( + fontSize: 15, + fontWeight: FontWeight.w500, + color: Colors.black87, + ), + ), + ), + const Icon(Icons.arrow_forward_ios_rounded, + size: 16, color: Colors.grey), + ], + ), + ), + ), + ], + ), + // Contact Information Section _buildSectionCard( title: 'Contact Information', diff --git a/lib/view/employees/employees_screen.dart b/lib/view/employees/employees_screen.dart index 5a6d925..db1f821 100644 --- a/lib/view/employees/employees_screen.dart +++ b/lib/view/employees/employees_screen.dart @@ -16,6 +16,7 @@ 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/view/employees/manage_reporting_bottom_sheet.dart'; class EmployeesScreen extends StatefulWidget { const EmployeesScreen({super.key}); @@ -64,13 +65,17 @@ class _EmployeesScreenState extends State with UIMixin { final searchQuery = query.toLowerCase(); final filtered = query.isEmpty ? List.from(employees) - : employees.where((e) => - e.name.toLowerCase().contains(searchQuery) || - e.email.toLowerCase().contains(searchQuery) || - e.phoneNumber.toLowerCase().contains(searchQuery) || - e.jobRole.toLowerCase().contains(searchQuery), - ).toList(); - filtered.sort((a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase())); + : employees + .where( + (e) => + e.name.toLowerCase().contains(searchQuery) || + e.email.toLowerCase().contains(searchQuery) || + e.phoneNumber.toLowerCase().contains(searchQuery) || + e.jobRole.toLowerCase().contains(searchQuery), + ) + .toList(); + filtered + .sort((a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase())); _filteredEmployees.assignAll(filtered); } @@ -106,7 +111,6 @@ class _EmployeesScreenState extends State with UIMixin { await _refreshEmployees(); } - @override Widget build(BuildContext context) { return Scaffold( @@ -160,7 +164,8 @@ class _EmployeesScreenState extends State with UIMixin { child: Row( 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), @@ -206,7 +211,8 @@ class _EmployeesScreenState extends State with UIMixin { Widget _buildFloatingActionButton() { return Obx(() { if (_permissionController.isLoading.value) return const SizedBox.shrink(); - final hasPermission = _permissionController.hasPermission(Permissions.manageEmployees); + final hasPermission = + _permissionController.hasPermission(Permissions.manageEmployees); if (!hasPermission) return const SizedBox.shrink(); return InkWell( @@ -218,7 +224,8 @@ class _EmployeesScreenState extends State with UIMixin { color: contentTheme.primary, borderRadius: BorderRadius.circular(28), boxShadow: const [ - BoxShadow(color: Colors.black26, blurRadius: 6, offset: Offset(0, 3)), + BoxShadow( + color: Colors.black26, blurRadius: 6, offset: Offset(0, 3)), ], ), child: const Row( @@ -235,33 +242,116 @@ class _EmployeesScreenState extends State with UIMixin { } 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), - hintText: 'Search employees...', - filled: true, - fillColor: Colors.white, - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - borderSide: BorderSide(color: Colors.grey.shade300, width: 1), + return Padding( + padding: MySpacing.xy(8, 8), + child: Row( + children: [ + // Search field + Expanded( + child: SizedBox( + height: 35, + child: TextField( + controller: _searchController, + style: const TextStyle(fontSize: 13, height: 1.2), + decoration: InputDecoration( + contentPadding: const EdgeInsets.symmetric(horizontal: 12), + prefixIcon: + const Icon(Icons.search, size: 20, color: Colors.grey), + suffixIcon: ValueListenableBuilder( + valueListenable: _searchController, + builder: (context, value, _) { + if (value.text.isEmpty) return const SizedBox.shrink(); + return IconButton( + icon: const Icon(Icons.clear, + size: 20, color: Colors.grey), + onPressed: () { + _searchController.clear(); + _filterEmployees(''); + }, + ); + }, + ), + hintText: 'Search employees...', + filled: true, + fillColor: Colors.white, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(5), + borderSide: BorderSide(color: Colors.grey.shade300), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(5), + borderSide: BorderSide(color: Colors.grey.shade300), + ), + ), + onChanged: (_) => _filterEmployees(_searchController.text), + ), + ), ), - suffixIcon: _searchController.text.isNotEmpty - ? GestureDetector( - onTap: () { - _searchController.clear(); - _filterEmployees(''); - }, - child: const Icon(Icons.close, size: 18, color: Colors.grey), - ) - : null, - ), - onChanged: (_) => _filterEmployees(_searchController.text), + MySpacing.width(10), + + // Three dots menu (Manage Reporting) + Container( + height: 35, + width: 35, + decoration: BoxDecoration( + color: Colors.white, + border: Border.all(color: Colors.grey.shade300), + borderRadius: BorderRadius.circular(5), + ), + child: PopupMenuButton( + padding: EdgeInsets.zero, + icon: + const Icon(Icons.more_vert, size: 20, color: Colors.black87), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(5)), + itemBuilder: (context) { + List> menuItems = []; + + // Section: Actions + menuItems.add( + const PopupMenuItem( + enabled: false, + height: 30, + child: Text( + "Actions", + style: TextStyle( + fontWeight: FontWeight.bold, color: Colors.grey), + ), + ), + ); + + // Manage Reporting option + menuItems.add( + PopupMenuItem( + value: 1, + child: Row( + children: [ + const Icon(Icons.manage_accounts_outlined, + size: 20, color: Colors.black87), + const SizedBox(width: 10), + const Expanded(child: Text("Manage Reporting")), + Icon(Icons.chevron_right, + size: 20, color: contentTheme.primary), + ], + ), + onTap: () { + Future.delayed(Duration.zero, () { + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (_) => const ManageReportingBottomSheet(), + ); + }); + }, + ), + ); + + return menuItems; + }, + ), + ), + ], ), ); } @@ -283,7 +373,8 @@ class _EmployeesScreenState extends State with UIMixin { 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]), ), ); } diff --git a/lib/view/employees/manage_reporting_bottom_sheet.dart b/lib/view/employees/manage_reporting_bottom_sheet.dart new file mode 100644 index 0000000..3eb5dd4 --- /dev/null +++ b/lib/view/employees/manage_reporting_bottom_sheet.dart @@ -0,0 +1,557 @@ +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/theme/app_theme.dart'; +import 'package:marco/controller/employee/employees_screen_controller.dart'; +import 'package:marco/model/employees/employee_model.dart'; +import 'package:marco/helpers/utils/base_bottom_sheet.dart'; +import 'package:marco/helpers/services/api_service.dart'; +import 'package:marco/helpers/widgets/my_snackbar.dart'; + +class ManageReportingBottomSheet extends StatefulWidget { + final EmployeeModel? initialEmployee; + final bool hideMainSelector; + final bool renderAsCard; + final bool hideLoggedUserFromSelection; // ✅ new + + const ManageReportingBottomSheet({ + super.key, + this.initialEmployee, + this.hideMainSelector = false, + this.renderAsCard = false, + this.hideLoggedUserFromSelection = false, // default false + }); + + @override + State createState() => + _ManageReportingBottomSheetState(); +} + +class _ManageReportingBottomSheetState + extends State { + final EmployeesScreenController _employeeController = Get.find(); + final TextEditingController _primaryController = TextEditingController(); + final TextEditingController _secondaryController = TextEditingController(); + + final RxList _filteredPrimary = [].obs; + final RxList _filteredSecondary = [].obs; + final RxList _selectedPrimary = [].obs; + final RxList _selectedSecondary = [].obs; + + final TextEditingController _selectEmployeeController = + TextEditingController(); + final RxList _filteredEmployees = [].obs; + EmployeeModel? _selectedEmployee; + + bool _isSubmitting = false; + + @override + void initState() { + super.initState(); + _primaryController + .addListener(() => _filterEmployees(_primaryController.text, true)); + _secondaryController + .addListener(() => _filterEmployees(_secondaryController.text, false)); + _selectEmployeeController + .addListener(() => _filterMainEmployee(_selectEmployeeController.text)); + + // if the parent passed an initialEmployee (profile page), preselect & load hierarchy + if (widget.initialEmployee != null) { + // delay to let widget finish first build + WidgetsBinding.instance.addPostFrameCallback((_) { + _onMainEmployeeSelected(widget.initialEmployee!); + }); + } + } + + @override + void dispose() { + _primaryController.dispose(); + _secondaryController.dispose(); + _selectEmployeeController.dispose(); + super.dispose(); + } + + void _filterMainEmployee(String query) { + final employees = _employeeController.employees; + final searchQuery = query.toLowerCase(); + + final filtered = query.isEmpty + ? [] + : employees + .where((e) => e.name.toLowerCase().contains(searchQuery)) + .take(6) + .toList(); + + _filteredEmployees.assignAll(filtered); + } + + void _filterEmployees(String query, bool isPrimary) { + final employees = _employeeController.employees; + final searchQuery = query.toLowerCase(); + + final filtered = query.isEmpty + ? [] + : employees + .where((e) => e.name.toLowerCase().contains(searchQuery)) + .take(6) + .toList(); + + if (isPrimary) { + _filteredPrimary.assignAll(filtered); + } else { + _filteredSecondary.assignAll(filtered); + } + } + + void _toggleSelection(EmployeeModel emp, bool isPrimary) { + final list = isPrimary ? _selectedPrimary : _selectedSecondary; + + if (isPrimary) { + //Allow only one primary employee at a time + list.clear(); + list.add(emp); + } else { + // ✅ Secondary employees can still have multiple selections + if (list.any((e) => e.id == emp.id)) { + list.removeWhere((e) => e.id == emp.id); + } else { + list.add(emp); + } + } + } + + /// helper to find employee by id from controller list (returns nullable) + EmployeeModel? _findEmployeeById(String id) { + for (final e in _employeeController.employees) { + if (e.id == id) return e; + } + return null; + } + + /// Called when user taps an employee from dropdown to manage reporting for. + /// It sets selected employee and fetches existing hierarchy to preselect reporters. + Future _onMainEmployeeSelected(EmployeeModel emp) async { + setState(() { + _selectedEmployee = emp; + _selectEmployeeController.text = emp.name; + _filteredEmployees.clear(); + }); + + // Clear previous selections + _selectedPrimary.clear(); + _selectedSecondary.clear(); + + // Fetch existing reporting hierarchy for this employee + try { + final data = await ApiService.getOrganizationHierarchyList(emp.id); + if (data == null || data.isEmpty) return; + + for (final item in data) { + try { + final isPrimary = item['isPrimary'] == true; + final reportTo = item['reportTo']; + if (reportTo == null) continue; + final reportToId = reportTo['id'] as String?; + if (reportToId == null) continue; + + final match = _findEmployeeById(reportToId); + if (match == null) continue; + + // ✅ Skip the employee whose profile is open + if (widget.initialEmployee != null && + match.id == widget.initialEmployee!.id) { + continue; + } + + if (isPrimary) { + if (!_selectedPrimary.any((e) => e.id == match.id)) { + _selectedPrimary.add(match); + } + } else { + if (!_selectedSecondary.any((e) => e.id == match.id)) { + _selectedSecondary.add(match); + } + } + } catch (_) { + // ignore malformed items + } + } + } catch (e) { + // Fetch failure - show a subtle snackbar + showAppSnackbar( + title: 'Error', + message: 'Failed to load existing reporting.', + type: SnackbarType.error); + } + } + + void _resetForm() { + setState(() { + _selectedEmployee = null; + _selectEmployeeController.clear(); + _selectedPrimary.clear(); + _selectedSecondary.clear(); + _filteredEmployees.clear(); + _filteredPrimary.clear(); + _filteredSecondary.clear(); + }); + } + + void _resetReportersOnly() { + _selectedPrimary.clear(); + _selectedSecondary.clear(); + _primaryController.clear(); + _secondaryController.clear(); + _filteredPrimary.clear(); + _filteredSecondary.clear(); + } + + Future _handleSubmit() async { + if (_selectedEmployee == null) { + showAppSnackbar( + title: 'Error', + message: 'Please select the employee.', + type: SnackbarType.error); + return; + } + if (_selectedPrimary.isEmpty) { + showAppSnackbar( + title: 'Error', + message: 'Please select at least one primary employee.', + type: SnackbarType.error); + return; + } + + final List> payload = []; + + for (final emp in _selectedPrimary) { + payload.add({ + "reportToId": emp.id, + "isPrimary": true, + "isActive": true, + }); + } + for (final emp in _selectedSecondary) { + payload.add({ + "reportToId": emp.id, + "isPrimary": false, + "isActive": true, + }); + } + + setState(() => _isSubmitting = true); + // show loader + Get.dialog(const Center(child: CircularProgressIndicator()), + barrierDismissible: false); + + final employeeId = _selectedEmployee!.id; + final success = await ApiService.manageOrganizationHierarchy( + employeeId: employeeId, + payload: payload, + ); + + // hide loader + if (Get.isDialogOpen == true) Get.back(); + + setState(() => _isSubmitting = false); + + if (success) { + showAppSnackbar( + title: 'Success', + message: 'Reporting assigned successfully', + type: SnackbarType.success); + + // Optionally refresh the saved hierarchy (not necessary here) but we can call: + await ApiService.getOrganizationHierarchyList(employeeId); + + // Keep sheet open and reset reporter selections for next assignment + _resetForm(); + } else { + showAppSnackbar( + title: 'Error', + message: 'Failed to assign reporting. Please try again.', + type: SnackbarType.error); + } + } + + void _handleCancel() => Navigator.pop(context); + + @override + Widget build(BuildContext context) { + // build the same child column content you already had, but assign to a variable + final Widget content = Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // conditional: show search section or simple header + if (widget.hideMainSelector) + _buildMainEmployeeHeader() + else + _buildMainEmployeeSection(), + + MySpacing.height(20), + + // Primary Employees section + _buildSearchSection( + label: "Primary Reporting Manager*", + controller: _primaryController, + filteredList: _filteredPrimary, + selectedList: _selectedPrimary, + isPrimary: true, + ), + + MySpacing.height(20), + + // Secondary Employees section + _buildSearchSection( + label: "Secondary Reporting Manager", + controller: _secondaryController, + filteredList: _filteredSecondary, + selectedList: _selectedSecondary, + isPrimary: false, + ), + ], + ); + + if (widget.renderAsCard) { + // Inline card for profile screen + return Card( + margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 0), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + elevation: 2, + child: Padding( + padding: const EdgeInsets.all(12), + child: content, + ), + ); + } + + // default: existing bottom sheet usage + return BaseBottomSheet( + title: "Manage Reporting", + submitText: "Submit", + isSubmitting: _isSubmitting, + onCancel: _handleCancel, + onSubmit: _handleSubmit, + child: content, + ); + } + + Widget _buildMainEmployeeHeader() { + // show selected employee name non-editable (chip-style) + final emp = _selectedEmployee; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + MyText.bodyMedium("Selected Employee", fontWeight: 600), + MySpacing.height(8), + if (emp != null) + Padding( + padding: const EdgeInsets.only(top: 8), + child: Chip( + label: Text(emp.name, style: const TextStyle(fontSize: 12)), + backgroundColor: Colors.indigo.shade50, + labelStyle: TextStyle(color: AppTheme.primaryColor), + deleteIcon: const Icon(Icons.close, size: 16), + onDeleted: () => setState(() { + _selectedEmployee = null; + _selectEmployeeController.clear(); + _resetReportersOnly(); + }), + ), + ) + else + const Text('No employee selected', + style: TextStyle(color: Colors.grey)), + ], + ); + } + + Widget _buildSearchSection({ + required String label, + required TextEditingController controller, + required RxList filteredList, + required RxList selectedList, + required bool isPrimary, + }) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + MyText.bodyMedium(label, fontWeight: 600), + MySpacing.height(8), + + // Search field + TextField( + controller: controller, + decoration: InputDecoration( + hintText: "Type to search employees...", + isDense: true, + filled: true, + fillColor: Colors.grey[50], + prefixIcon: const Icon(Icons.search, size: 18, color: Colors.grey), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(6), + borderSide: BorderSide(color: Colors.grey.shade300), + ), + ), + ), + + // Dropdown suggestions + Obx(() { + if (filteredList.isEmpty) return const SizedBox.shrink(); + return Container( + margin: const EdgeInsets.only(top: 4), + decoration: BoxDecoration( + color: Colors.white, + border: Border.all(color: Colors.grey.shade300), + borderRadius: BorderRadius.circular(6), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 4, + offset: const Offset(0, 2), + ) + ], + ), + child: ListView.builder( + itemCount: filteredList.length, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemBuilder: (_, index) { + final emp = filteredList[index]; + final isSelected = selectedList.any((e) => e.id == emp.id); + return ListTile( + dense: true, + leading: CircleAvatar( + radius: 14, + backgroundColor: AppTheme.primaryColor.withOpacity(0.1), + child: MyText.labelSmall( + emp.name.isNotEmpty ? emp.name[0].toUpperCase() : "?", + fontWeight: 600, + color: AppTheme.primaryColor, + ), + ), + title: Text(emp.name, style: const TextStyle(fontSize: 13)), + trailing: Icon( + isSelected + ? Icons.check_circle + : Icons.radio_button_unchecked, + color: isSelected ? AppTheme.primaryColor : Colors.grey, + size: 18, + ), + onTap: () { + _toggleSelection(emp, isPrimary); + filteredList.clear(); + controller.clear(); + }, + ); + }, + ), + ); + }), + + MySpacing.height(10), + + // Selected employees as chips + Obx(() { + if (selectedList.isEmpty) return const SizedBox.shrink(); + return Wrap( + spacing: 6, + runSpacing: 6, + children: selectedList.map((emp) { + return Chip( + label: Text(emp.name, style: const TextStyle(fontSize: 12)), + backgroundColor: Colors.indigo.shade50, + labelStyle: TextStyle(color: AppTheme.primaryColor), + deleteIcon: const Icon(Icons.close, size: 16), + onDeleted: () => selectedList.remove(emp), + ); + }).toList(), + ); + }), + ], + ); + } + + Widget _buildMainEmployeeSection() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + MyText.bodyMedium("Select Employee *", fontWeight: 600), + MySpacing.height(8), + TextField( + controller: _selectEmployeeController, + decoration: InputDecoration( + hintText: "Type to search employee...", + isDense: true, + filled: true, + fillColor: Colors.grey[50], + prefixIcon: const Icon(Icons.search, size: 18, color: Colors.grey), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(6), + borderSide: BorderSide(color: Colors.grey.shade300), + ), + ), + ), + Obx(() { + if (_filteredEmployees.isEmpty) return const SizedBox.shrink(); + return Container( + margin: const EdgeInsets.only(top: 4), + decoration: BoxDecoration( + color: Colors.white, + border: Border.all(color: Colors.grey.shade300), + borderRadius: BorderRadius.circular(6), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 4, + offset: const Offset(0, 2), + ) + ], + ), + child: ListView.builder( + itemCount: _filteredEmployees.length, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemBuilder: (_, index) { + final emp = _filteredEmployees[index]; + return ListTile( + dense: true, + leading: CircleAvatar( + radius: 14, + backgroundColor: AppTheme.primaryColor.withOpacity(0.1), + child: MyText.labelSmall( + emp.name.isNotEmpty ? emp.name[0].toUpperCase() : "?", + fontWeight: 600, + color: AppTheme.primaryColor, + ), + ), + title: Text(emp.name, style: const TextStyle(fontSize: 13)), + onTap: () => _onMainEmployeeSelected(emp), + ); + }, + ), + ); + }), + if (_selectedEmployee != null) + Padding( + padding: const EdgeInsets.only(top: 8), + child: Chip( + label: Text(_selectedEmployee!.name, + style: const TextStyle(fontSize: 12)), + backgroundColor: Colors.indigo.shade50, + labelStyle: TextStyle(color: AppTheme.primaryColor), + deleteIcon: const Icon(Icons.close, size: 16), + onDeleted: () => setState(() { + _selectedEmployee = null; + _selectEmployeeController.clear(); + // clear selected reporters too, since employee changed + _resetReportersOnly(); + }), + ), + ), + ], + ); + } +} diff --git a/lib/view/finance/advance_payment_screen.dart b/lib/view/finance/advance_payment_screen.dart index 96d14d3..2c1fe40 100644 --- a/lib/view/finance/advance_payment_screen.dart +++ b/lib/view/finance/advance_payment_screen.dart @@ -190,11 +190,6 @@ class _AdvancePaymentScreenState extends State ), ), ), - const SizedBox(width: 4), - IconButton( - icon: const Icon(Icons.tune, color: Colors.black), - onPressed: () {}, - ), ], ), );