import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:on_field_work/helpers/widgets/my_spacing.dart'; import 'package:on_field_work/helpers/widgets/my_text.dart'; import 'package:on_field_work/helpers/theme/app_theme.dart'; import 'package:on_field_work/controller/employee/employees_screen_controller.dart'; import 'package:on_field_work/model/employees/employee_model.dart'; import 'package:on_field_work/helpers/utils/base_bottom_sheet.dart'; import 'package:on_field_work/helpers/services/api_service.dart'; import 'package:on_field_work/helpers/widgets/my_snackbar.dart'; class ManageReportingBottomSheet extends StatefulWidget { final EmployeeModel? initialEmployee; final bool hideMainSelector; final bool renderAsCard; final bool hideLoggedUserFromSelection; final String? loggedUserId; const ManageReportingBottomSheet({ super.key, this.initialEmployee, this.hideMainSelector = false, this.renderAsCard = false, this.hideLoggedUserFromSelection = false, this.loggedUserId, }); @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 employeeId = _selectedEmployee!.id; // === BUILD PAYLOAD (updated logic) === // fetch current assignments so we can deactivate old ones List? currentAssignments; try { currentAssignments = await ApiService.getOrganizationHierarchyList(employeeId); } catch (_) { currentAssignments = null; } // helper sets of newly selected ids final newPrimaryIds = _selectedPrimary.map((e) => e.id).toSet(); final newSecondaryIds = _selectedSecondary.map((e) => e.id).toSet(); final List> payload = []; // 1) For current active assignments: if they are not in new selections -> add isActive:false if (currentAssignments != null && currentAssignments.isNotEmpty) { for (final item in currentAssignments) { try { final reportTo = item['reportTo']; if (reportTo == null) continue; final reportToId = reportTo['id'] as String?; if (reportToId == null) continue; final isPrimary = item['isPrimary'] == true; final currentlyActive = item['isActive'] == true || item['isActive'] == null; // be safe // if currently active and not included in new selection -> mark false if (currentlyActive) { if (isPrimary && !newPrimaryIds.contains(reportToId)) { payload.add({ "reportToId": reportToId, "isPrimary": true, "isActive": false, }); } else if (!isPrimary && !newSecondaryIds.contains(reportToId)) { payload.add({ "reportToId": reportToId, "isPrimary": false, "isActive": false, }); } } } catch (_) { // ignore malformed items (same behavior as before) } } } // 2) Add new primary (active) for (final emp in _selectedPrimary) { payload.add({ "reportToId": emp.id, "isPrimary": true, "isActive": true, }); } // 3) Add new secondary (active) 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 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, ); // ✅ Refresh both the organization hierarchy and employee details so UI shows updated managers try { final empId = employeeId; final EmployeesScreenController controller = Get.find(); await controller.fetchReportingManagers(empId); await controller.fetchEmployeeDetails(empId); } catch (_) {} // Optional: re-fetch the organization hierarchy list (if needed elsewhere) await ApiService.getOrganizationHierarchyList(employeeId); _resetForm(); if (Navigator.of(context).canPop()) { Navigator.of(context).pop(); } } 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, ), ], ); // 🔥 WRAP EVERYTHING IN SAFEAREA + SCROLL + BOTTOM PADDING final safeWrappedContent = SafeArea( child: SingleChildScrollView( padding: EdgeInsets.only( bottom: MediaQuery.of(context).viewPadding.bottom + 20, left: 16, right: 16, top: 8, ), child: content, ), ); 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: safeWrappedContent, ), ); } // default: existing bottom sheet usage return BaseBottomSheet( title: "Manage Reporting", submitText: "Submit", isSubmitting: _isSubmitting, onCancel: _handleCancel, onSubmit: _handleSubmit, child: safeWrappedContent, ); } 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), ), ) 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) { final isProfileEmployee = widget.initialEmployee != null && emp.id == widget.initialEmployee!.id; return Chip( label: Text(emp.name, style: const TextStyle(fontSize: 12)), backgroundColor: isProfileEmployee ? Colors.indigo.shade50 : Colors.indigo.shade50, labelStyle: TextStyle(color: AppTheme.primaryColor), // Only show delete icon / action for non-profile employees deleteIcon: isProfileEmployee ? null : const Icon(Icons.close, size: 16), onDeleted: isProfileEmployee ? null : () => 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(); }), ), ), ], ); } }