diff --git a/lib/model/dailyTaskPlanning/assign_task_bottom_sheet .dart b/lib/model/dailyTaskPlanning/assign_task_bottom_sheet .dart index d0509ee..a02f33e 100644 --- a/lib/model/dailyTaskPlanning/assign_task_bottom_sheet .dart +++ b/lib/model/dailyTaskPlanning/assign_task_bottom_sheet .dart @@ -15,10 +15,8 @@ import 'package:marco/helpers/widgets/tenant/organization_selector.dart'; import 'package:marco/helpers/widgets/tenant/service_selector.dart'; import 'package:marco/model/attendance/organization_per_project_list_model.dart'; import 'package:marco/model/tenant/tenant_services_model.dart'; - -// Added imports for employee selection import 'package:marco/model/employees/employee_model.dart'; -import 'package:marco/model/employees/multiple_select_bottomsheet.dart'; +import 'package:marco/model/employees/multiple_select_role_bottomsheet.dart'; class AssignTaskBottomSheet extends StatefulWidget { final String workLocation; @@ -87,8 +85,10 @@ class _AssignTaskBottomSheetState extends State { serviceId: selectedService?.id, organizationId: selectedOrganization?.id, ); - await controller.fetchTaskData(selectedProjectId, - serviceId: selectedService?.id); + await controller.fetchTaskData( + selectedProjectId, + serviceId: selectedService?.id, + ); } @override @@ -145,13 +145,10 @@ class _AssignTaskBottomSheetState extends State { _infoRow(Icons.location_on, "Work Location", "${widget.buildingName} > ${widget.floorName} > ${widget.workAreaName} > ${widget.activityName}"), const Divider(), - - // Pending Task Info _infoRow( Icons.pending_actions, "Pending Task", "${widget.pendingTask}"), const Divider(), - // Role Selector (kept as-is) GestureDetector( onTap: _onRoleMenuPressed, child: Row(children: [ @@ -163,12 +160,7 @@ class _AssignTaskBottomSheetState extends State { MySpacing.height(8), - // ------------------------------- - // Employee selector (REPLACED) - // ------------------------------- - // We show a button-like container (with border) that opens the reusable - // EmployeeSelectionBottomSheet. Selected employees are reflected using - // existing controller.uploadingStates & controller.selectedEmployees. + /// TEAM SELECT BOX GestureDetector( onTap: _openEmployeeSelectionSheet, child: Container( @@ -178,37 +170,44 @@ class _AssignTaskBottomSheetState extends State { borderRadius: BorderRadius.circular(6), ), child: Row( - children: [ - const Icon(Icons.group, color: Colors.black54), - const SizedBox(width: 10), - Expanded( - child: Obx(() { - final count = controller.selectedEmployees.length; - if (count == 0) { - return MyText("Select team members", - color: Colors.grey.shade700); - } - // show summary text when there are selected employees - final names = controller.selectedEmployees - .map((e) => e.name) - .join(", "); - return Text( - names, - maxLines: 2, - overflow: TextOverflow.ellipsis, - style: const TextStyle(fontSize: 14), - ); - }), - ), - const SizedBox(width: 8), - const Icon(Icons.arrow_drop_down, color: Colors.grey), - ], - ), + children: [ + const Icon(Icons.group, color: Colors.black54), + const SizedBox(width: 10), + + // Expanded name area + Expanded( + child: Obx(() { + final count = controller.selectedEmployees.length; + if (count == 0) { + return MyText( + "Select team members", + color: Colors.grey.shade700, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ); + } + + final names = controller.selectedEmployees + .map((e) => e.name) + .join(", "); + + return Text( + names, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: const TextStyle(fontSize: 14), + ); + }), + ), + + const Icon(Icons.arrow_drop_down, color: Colors.grey), + ], +) + ), ), - MySpacing.height(8), - // Selected Employees Chips (keeps existing behavior) + MySpacing.height(8), _buildSelectedEmployees(), MySpacing.height(8), @@ -258,13 +257,13 @@ class _AssignTaskBottomSheetState extends State { ), ], ).then((value) { - if (value != null) - controller.onRoleSelected(value == 'all' ? null : value); + if (value != null) { + selectedRoleId = value == 'all' ? null : value; + controller.onRoleSelected(selectedRoleId); + } }); } - // Removed old inline employee list; selection handled by bottom sheet. - Widget _buildSelectedEmployees() { return Obx(() { if (controller.selectedEmployees.isEmpty) return Container(); @@ -310,7 +309,9 @@ class _AssignTaskBottomSheetState extends State { maxLines: maxLines, decoration: InputDecoration( hintText: hintText, - border: OutlineInputBorder(borderRadius: BorderRadius.circular(6)), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(6), + ), contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), ), @@ -335,11 +336,16 @@ class _AssignTaskBottomSheetState extends State { text: TextSpan( children: [ WidgetSpan( - child: MyText.titleMedium("$title: ", - fontWeight: 600, color: Colors.black), + child: MyText.titleMedium( + "$title: ", + fontWeight: 600, + color: Colors.black, + ), ), TextSpan( - text: value, style: const TextStyle(color: Colors.black)), + text: value, + style: const TextStyle(color: Colors.black), + ), ], ), ), @@ -350,46 +356,42 @@ class _AssignTaskBottomSheetState extends State { } Future _openEmployeeSelectionSheet() async { - // Open the existing EmployeeSelectionBottomSheet final result = await showModalBottomSheet>( context: context, isScrollControlled: true, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(20)), ), - builder: (context) => EmployeeSelectionBottomSheet( - initiallySelected: controller.selectedEmployees.toList(), - multipleSelection: true, - title: 'Select Team Members', - ), + builder: (context) { + return DraggableScrollableSheet( + expand: false, + initialChildSize: 0.85, + minChildSize: 0.6, + maxChildSize: 1.0, + builder: (_, scrollController) { + return Container( + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.vertical(top: Radius.circular(20)), + ), + + child: MultipleSelectRoleBottomSheet( + projectId: selectedProjectId!, + organizationId: selectedOrganization?.id, + serviceId: selectedService?.id, + roleId: selectedRoleId, + initiallySelected: controller.selectedEmployees.toList(), + scrollController: scrollController, + ), + ); + }, + ); + }, ); - if (result == null) return; - - // Merge returned employees into controller.uploadingStates & controller.selectedEmployees - // 1) Reset all uploadingStates to false, then set true for selected - controller.uploadingStates.forEach((key, rx) { - rx.value = false; - }); - - for (final emp in result) { - final idStr = emp.id.toString(); - if (controller.uploadingStates.containsKey(idStr)) { - controller.uploadingStates[idStr]?.value = true; - } else { - // if uploadingStates doesn't have the id yet, add it (safe fallback) - controller.uploadingStates[idStr] = RxBool(true); - } - } - - // 2) Update selectedEmployees list in controller - controller.selectedEmployees.assignAll(result); - - // 3) Call controller helper (keeps existing behavior) - try { + if (result != null) { + controller.selectedEmployees.assignAll(result); controller.updateSelectedEmployees(); - } catch (_) { - // If controller does not implement updateSelectedEmployees, ignore. } } @@ -398,18 +400,20 @@ class _AssignTaskBottomSheetState extends State { if (selectedTeam.isEmpty) { showAppSnackbar( - title: "Team Required", - message: "Please select at least one team member", - type: SnackbarType.error); + title: "Team Required", + message: "Please select at least one team member", + type: SnackbarType.error, + ); return; } final target = double.tryParse(targetController.text.trim()); if (target == null || target <= 0) { showAppSnackbar( - title: "Invalid Input", - message: "Please enter a valid target number", - type: SnackbarType.error); + title: "Invalid Input", + message: "Please enter a valid target number", + type: SnackbarType.error, + ); return; } @@ -425,9 +429,10 @@ class _AssignTaskBottomSheetState extends State { final description = descriptionController.text.trim(); if (description.isEmpty) { showAppSnackbar( - title: "Description Required", - message: "Please enter a description", - type: SnackbarType.error); + title: "Description Required", + message: "Please enter a description", + type: SnackbarType.error, + ); return; } diff --git a/lib/model/employees/multiple_select_bottomsheet.dart b/lib/model/employees/multiple_select_bottomsheet.dart index 9685a4f..e7d0344 100644 --- a/lib/model/employees/multiple_select_bottomsheet.dart +++ b/lib/model/employees/multiple_select_bottomsheet.dart @@ -200,6 +200,26 @@ class _EmployeeSelectionBottomSheetState final emp = results[index]; final isSelected = _selectedEmployees.contains(emp); + Widget trailingWidget; + + if (widget.multipleSelection) { + // Multiple selection → normal checkbox + trailingWidget = Checkbox( + value: isSelected, + onChanged: (_) => _toggleEmployee(emp), + fillColor: MaterialStateProperty.resolveWith( + (states) => states.contains(MaterialState.selected) + ? Colors.blueAccent + : Colors.white, + ), + ); + } else { + // Single selection → check circle + trailingWidget = isSelected + ? const Icon(Icons.check_circle, color: Colors.blueAccent) + : const Icon(Icons.circle_outlined, color: Colors.grey); + } + return ListTile( leading: CircleAvatar( backgroundColor: Colors.blueAccent, @@ -211,15 +231,7 @@ class _EmployeeSelectionBottomSheetState ), title: Text('${emp.firstName} ${emp.lastName}'), subtitle: Text(emp.email), - trailing: Checkbox( - value: isSelected, - onChanged: (_) => _toggleEmployee(emp), - fillColor: MaterialStateProperty.resolveWith( - (states) => states.contains(MaterialState.selected) - ? Colors.blueAccent - : Colors.white, - ), - ), + trailing: trailingWidget, onTap: () => _toggleEmployee(emp), contentPadding: const EdgeInsets.symmetric(horizontal: 0, vertical: 4), diff --git a/lib/model/employees/multiple_select_role_bottomsheet.dart b/lib/model/employees/multiple_select_role_bottomsheet.dart index a1f4951..1ffd1e0 100644 --- a/lib/model/employees/multiple_select_role_bottomsheet.dart +++ b/lib/model/employees/multiple_select_role_bottomsheet.dart @@ -59,10 +59,12 @@ class _MultipleSelectRoleBottomSheetState List employees = controller.employees.toList(); if (widget.roleId != null && widget.roleId!.isNotEmpty) { - employees = - employees.where((emp) => emp.jobRoleID == widget.roleId).toList(); + employees = employees + .where((emp) => emp.jobRoleID == widget.roleId) + .toList(); } + // Selected first employees.sort((a, b) { final aSel = _selected.any((e) => e.id == a.id) ? 0 : 1; final bSel = _selected.any((e) => e.id == b.id) ? 0 : 1; @@ -91,6 +93,7 @@ class _MultipleSelectRoleBottomSheetState ); } + // Selected on top _filtered.sort((a, b) { final aSel = _selected.any((e) => e.id == a.id) ? 0 : 1; final bSel = _selected.any((e) => e.id == b.id) ? 0 : 1; @@ -108,8 +111,8 @@ class _MultipleSelectRoleBottomSheetState _selected.add(emp); } } else { - // Single selection → return immediately - Get.back(result: [emp]); + _selected.assignAll([emp]); + Get.back(result: _selected); } _onSearch(_searchController.text.trim()); @@ -148,17 +151,49 @@ class _MultipleSelectRoleBottomSheetState ), ); + /// ⭐ NEW — Chips showing selected employees + Widget _selectedChips() { + return Obx(() { + if (_selected.isEmpty) return const SizedBox(); + + return Wrap( + spacing: 8, + runSpacing: 8, + children: _selected.map((emp) { + return Chip( + label: Text(emp.name), + deleteIcon: const Icon(Icons.close), + onDeleted: () { + _selected.remove(emp); + _onSearch(_searchController.text.trim()); + }, + backgroundColor: Colors.blue.shade50, + ); + }).toList(), + ); + }); + } + @override Widget build(BuildContext context) { return BaseBottomSheet( title: widget.title, onCancel: () => Get.back(), - onSubmit: () => Get.back(result: _selected.toList()), // Return plain list + onSubmit: () => Get.back(result: _selected), child: SizedBox( height: MediaQuery.of(context).size.height * 0.55, child: Column( children: [ _searchBar(), + + /// ⭐ Chips shown right below search bar + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: _selectedChips(), + ), + + const SizedBox(height: 6), + Expanded( child: Obx(() { if (_isLoading.value) { @@ -191,15 +226,6 @@ class _MultipleSelectRoleBottomSheetState trailing: Checkbox( value: isSelected, onChanged: (_) => _onTap(emp), - fillColor: - MaterialStateProperty.resolveWith((states) { - if (states.contains(MaterialState.selected)) { - return Colors.blueAccent; - } - return Colors.white; - }), - checkColor: Colors.white, - side: const BorderSide(color: Colors.grey), ), contentPadding: const EdgeInsets.symmetric( horizontal: 4, diff --git a/lib/model/finance/add_payment_request_bottom_sheet.dart b/lib/model/finance/add_payment_request_bottom_sheet.dart index a0a3186..a23bfcf 100644 --- a/lib/model/finance/add_payment_request_bottom_sheet.dart +++ b/lib/model/finance/add_payment_request_bottom_sheet.dart @@ -208,7 +208,7 @@ class _PaymentRequestBottomSheetState extends State<_PaymentRequestBottomSheet> ? null : "Enter valid amount"), _gap(), - _buildPayeeAutocompleteField(), + _buildPayeeField(), _gap(), _buildDropdown( "Currency", @@ -353,67 +353,27 @@ class _PaymentRequestBottomSheetState extends State<_PaymentRequestBottomSheet> return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - SectionTitle( - icon: Icons.person_outline, title: "Payee", requiredField: true), - const SizedBox(height: 6), - Autocomplete( - optionsBuilder: (textEditingValue) { - final query = textEditingValue.text.toLowerCase(); - return query.isEmpty - ? const Iterable.empty() - : controller.payees - .where((p) => p.toLowerCase().contains(query)); - }, - displayStringForOption: (option) => option, - fieldViewBuilder: - (context, fieldController, focusNode, onFieldSubmitted) { - WidgetsBinding.instance.addPostFrameCallback((_) { - if (fieldController.text != controller.selectedPayee.value) { - fieldController.text = controller.selectedPayee.value; - fieldController.selection = TextSelection.fromPosition( - TextPosition(offset: fieldController.text.length)); - } - }); - - return TextFormField( - controller: fieldController, - focusNode: focusNode, - decoration: InputDecoration( - hintText: "Type or select payee", - filled: true, - fillColor: Colors.grey.shade100, - contentPadding: - const EdgeInsets.symmetric(horizontal: 12, vertical: 14), - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - borderSide: BorderSide(color: Colors.grey.shade300), + const SectionTitle( + icon: Icons.person_outline, + title: "Payee", + requiredField: true, + ), + MySpacing.height(6), + GestureDetector( + onTap: _showPayeeSelector, + child: TileContainer( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Obx(() => Text( + controller.selectedPayee.value?.name ?? "Select Payee", + style: const TextStyle(fontSize: 15), + overflow: TextOverflow.ellipsis, + )), ), - ), - validator: (v) => - v == null || v.trim().isEmpty ? "Please enter payee" : null, - onChanged: (val) => controller.selectedPayee.value = val, - ); - }, - onSelected: (selection) => controller.selectedPayee.value = selection, - optionsViewBuilder: (context, onSelected, options) => Material( - color: Colors.white, - elevation: 4, - borderRadius: BorderRadius.circular(8), - child: ConstrainedBox( - constraints: const BoxConstraints(maxHeight: 200, minWidth: 300), - child: ListView.builder( - padding: EdgeInsets.zero, - itemCount: options.length, - itemBuilder: (_, index) => InkWell( - onTap: () => onSelected(options.elementAt(index)), - child: Container( - padding: const EdgeInsets.symmetric( - vertical: 10, horizontal: 12), - child: Text(options.elementAt(index), - style: const TextStyle(fontSize: 14)), - ), - ), - ), + const Icon(Icons.arrow_drop_down, size: 22), + ], ), ), ),